From dbab427d69ae3964eaa5ed6467ffa1365ab1e550 Mon Sep 17 00:00:00 2001 From: jtalborough Date: Tue, 25 Feb 2025 14:34:28 -0500 Subject: [PATCH 001/161] fix: update package references and clean up unused imports --- .../Global/Scheduler.cs | 1 - .../VideoCodec/VideoCodecBase.cs | 4 +-- src/PepperDash.Essentials/ControlSystem.cs | 25 +++++-------------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Global/Scheduler.cs b/src/PepperDash.Essentials.Core/Global/Scheduler.cs index d3641ffc..c946b67d 100644 --- a/src/PepperDash.Essentials.Core/Global/Scheduler.cs +++ b/src/PepperDash.Essentials.Core/Global/Scheduler.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp; -using Crestron.SimplSharp.Reflection; using Crestron.SimplSharp.Scheduler; using PepperDash.Core; diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index b0b5ef55..fbc73852 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharp; using PepperDash.Core; using PepperDash.Core.Intersystem; using PepperDash.Core.Intersystem.Tokens; @@ -20,6 +19,7 @@ using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; using Serilog.Events; using Feedback = PepperDash.Essentials.Core.Feedback; +using System.Text; namespace PepperDash.Essentials.Devices.Common.VideoCodec { diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index ef19c955..6fbcf06c 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -150,7 +150,7 @@ namespace PepperDash.Essentials var dirSeparator = Global.DirectorySeparator; - string directoryPrefix; + string directoryPrefix; directoryPrefix = Directory.GetApplicationRootDirectory(); @@ -158,24 +158,10 @@ namespace PepperDash.Essentials if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Server) // Handles 3-series running Windows CE OS { - string userFolder; - string nvramFolder; - bool is4series = false; + string userFolder = "user"; + string nvramFolder = "nvram"; - if (eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4)) // Handle 4-series - { - is4series = true; - // Set path to user/ - userFolder = "user"; - nvramFolder = "nvram"; - } - else - { - userFolder = "User"; - nvramFolder = "Nvram"; - } - - Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series"); + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, "4-series"); //Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{0} on {1} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series"); // Check if User/ProgramX exists @@ -337,8 +323,9 @@ namespace PepperDash.Essentials Debug.LogMessage(LogEventLevel.Information, "Tear down COMPLETE"); } + /// - /// + /// Load method /// void Load() { From 8be5481ac969b0036c8647f8554849bed8c6a0c8 Mon Sep 17 00:00:00 2001 From: jtalborough Date: Tue, 25 Feb 2025 19:17:34 -0500 Subject: [PATCH 002/161] fix: update target frameworks and package references; change culture to InvariantCulture --- src/Directory.Build.targets | 6 ++-- .../Comm/ControlPropertiesConfig.cs | 11 ++++-- .../Comm/GenericTcpIpClient.cs | 7 ++-- src/PepperDash.Core/Comm/GenericUdpServer.cs | 5 ++- .../Comm/TcpClientConfigObject.cs | 4 ++- src/PepperDash.Core/CommunicationExtras.cs | 10 ++++-- .../Config/PortalConfigReader.cs | 16 ++++++--- src/PepperDash.Core/CoreInterfaces.cs | 6 ++-- src/PepperDash.Core/EthernetHelper.cs | 6 ++-- .../JsonToSimplArrayLookupChild.cs | 8 +++-- .../JsonToSimpl/JsonToSimplChildObjectBase.cs | 8 +++-- .../JsonToSimpl/JsonToSimplFileMaster.cs | 8 +++-- .../JsonToSimpl/JsonToSimplGenericMaster.cs | 7 ++-- .../JsonToSimpl/JsonToSimplMaster.cs | 13 ++++--- .../JsonToSimplPortalFileMaster.cs | 7 ++-- src/PepperDash.Core/Logging/Debug.cs | 7 ++-- src/PepperDash.Core/Logging/DebugContext.cs | 7 ++-- src/PepperDash.Core/Logging/DebugMemory.cs | 6 ++-- .../Logging/DebugWebsocketSink.cs | 6 ++-- src/PepperDash.Core/PepperDash.Core.csproj | 7 +++- src/PepperDash.Core/Web/WebApiServer.cs | 9 +++-- .../WebApi/Presets/WebApiPasscodeClient.cs | 8 +++-- .../Global/Global.cs | 5 +-- .../PepperDash.Essentials.Core.csproj | 2 +- ...epperDash.Essentials.Devices.Common.csproj | 2 +- ...Essentials.MobileControl.Messengers.csproj | 2 +- ...PepperDash.Essentials.MobileControl.csproj | 2 +- src/PepperDash.Essentials/ControlSystem.cs | 35 +++++++++++++++++-- .../PepperDash.Essentials.csproj | 4 +-- 29 files changed, 160 insertions(+), 64 deletions(-) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index ce6c0eed..50816cf4 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,14 +1,14 @@ - + true build; - + true build; - + true build; diff --git a/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs b/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs index c665dad8..a7d2300a 100644 --- a/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs +++ b/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs @@ -1,7 +1,12 @@ -using System; +extern alias NewtonsoftJson; + +using System; using Crestron.SimplSharp; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute; +using JsonIgnore = NewtonsoftJson::Newtonsoft.Json.JsonIgnoreAttribute; +using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; +using NullValueHandling = NewtonsoftJson::Newtonsoft.Json.NullValueHandling; +using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter; namespace PepperDash.Core { diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs index d13eed3f..16963099 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs @@ -1,10 +1,13 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; -using Newtonsoft.Json; +using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; +using Required = NewtonsoftJson::Newtonsoft.Json.Required; namespace PepperDash.Core { diff --git a/src/PepperDash.Core/Comm/GenericUdpServer.cs b/src/PepperDash.Core/Comm/GenericUdpServer.cs index a713872a..34df1444 100644 --- a/src/PepperDash.Core/Comm/GenericUdpServer.cs +++ b/src/PepperDash.Core/Comm/GenericUdpServer.cs @@ -1,12 +1,15 @@  +extern alias NewtonsoftJson; + using System; using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; -using Newtonsoft.Json; +using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using PepperDash.Core.Logging; +using Required = NewtonsoftJson::Newtonsoft.Json.Required; namespace PepperDash.Core { diff --git a/src/PepperDash.Core/Comm/TcpClientConfigObject.cs b/src/PepperDash.Core/Comm/TcpClientConfigObject.cs index 226473bf..e2137d4a 100644 --- a/src/PepperDash.Core/Comm/TcpClientConfigObject.cs +++ b/src/PepperDash.Core/Comm/TcpClientConfigObject.cs @@ -1,4 +1,6 @@ -using Newtonsoft.Json; +extern alias NewtonsoftJson; + +using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; namespace PepperDash.Core { diff --git a/src/PepperDash.Core/CommunicationExtras.cs b/src/PepperDash.Core/CommunicationExtras.cs index a8bc57d3..1fcce75b 100644 --- a/src/PepperDash.Core/CommunicationExtras.cs +++ b/src/PepperDash.Core/CommunicationExtras.cs @@ -1,8 +1,12 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; -using Newtonsoft.Json; +using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute; +using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; +using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter; namespace PepperDash.Core { @@ -88,7 +92,7 @@ namespace PepperDash.Core /// The current socket status of the client /// [JsonProperty("clientStatus")] - [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + [JsonConverter(typeof(StringEnumConverter))] SocketStatus ClientStatus { get; } } diff --git a/src/PepperDash.Core/Config/PortalConfigReader.cs b/src/PepperDash.Core/Config/PortalConfigReader.cs index 41ca2cbd..b7c5c784 100644 --- a/src/PepperDash.Core/Config/PortalConfigReader.cs +++ b/src/PepperDash.Core/Config/PortalConfigReader.cs @@ -1,12 +1,18 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Linq; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; +using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray; +using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; +using JToken = NewtonsoftJson::Newtonsoft.Json.Linq.JToken; using PepperDash.Core; using Serilog.Events; + + namespace PepperDash.Core.Config { @@ -56,12 +62,12 @@ namespace PepperDash.Core.Config var merged = MergeConfigs(jsonObj); if (jsonObj[systemUrl] != null) { - merged[systemUrl] = jsonObj[systemUrl].Value(); + merged[systemUrl] = (string)jsonObj[systemUrl]; } if (jsonObj[templateUrl] != null) { - merged[templateUrl] = jsonObj[templateUrl].Value(); + merged[templateUrl] = (string)jsonObj[templateUrl]; } jsonObj = merged; diff --git a/src/PepperDash.Core/CoreInterfaces.cs b/src/PepperDash.Core/CoreInterfaces.cs index 6e0b639e..c334cc00 100644 --- a/src/PepperDash.Core/CoreInterfaces.cs +++ b/src/PepperDash.Core/CoreInterfaces.cs @@ -1,9 +1,11 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using System.Linq; using System.Text; using Crestron.SimplSharp; -using Newtonsoft.Json; +using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using Serilog; namespace PepperDash.Core diff --git a/src/PepperDash.Core/EthernetHelper.cs b/src/PepperDash.Core/EthernetHelper.cs index 76a9405c..20709eb1 100644 --- a/src/PepperDash.Core/EthernetHelper.cs +++ b/src/PepperDash.Core/EthernetHelper.cs @@ -1,5 +1,7 @@ -using Crestron.SimplSharp; -using Newtonsoft.Json; +extern alias NewtonsoftJson; + +using Crestron.SimplSharp; +using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using Serilog.Events; namespace PepperDash.Core diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs index 268f6207..559fc87d 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs @@ -1,6 +1,8 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Linq; -using Newtonsoft.Json.Linq; +using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray; using Serilog.Events; namespace PepperDash.Core.JsonToSimpl @@ -129,7 +131,7 @@ namespace PepperDash.Core.JsonToSimpl var item = array.FirstOrDefault(o => { var prop = o[SearchPropertyName]; - return prop != null && prop.Value() + return prop != null && ((string)prop) .Equals(SearchPropertyValue, StringComparison.OrdinalIgnoreCase); }); if (item == null) diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs index 68e4bf91..e7da9c9c 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs @@ -1,7 +1,9 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json.Linq; +using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; namespace PepperDash.Core.JsonToSimpl { @@ -233,7 +235,7 @@ namespace PepperDash.Core.JsonToSimpl if (isCount) response = (t.HasValues ? t.Children().Count() : 0).ToString(); else - response = t.Value(); + response = (string)t; Debug.Console(1, " ='{0}'", response); return true; } diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs index 13354fdd..bf44e2b2 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs @@ -1,11 +1,15 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; -using Newtonsoft.Json.Linq; +using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; +using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; +using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; namespace PepperDash.Core.JsonToSimpl { diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs index 14f7eaa8..c065f577 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs @@ -1,7 +1,10 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using Crestron.SimplSharp; -using Newtonsoft.Json.Linq; +using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; +using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; namespace PepperDash.Core.JsonToSimpl { diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs index b3fa92a3..bc965e92 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs @@ -1,10 +1,15 @@ +extern alias NewtonsoftJson; + using System; using System.Collections.Generic; using System.IO; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray; +using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; +using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; +using JsonSerializationException = NewtonsoftJson::Newtonsoft.Json.JsonSerializationException; +using JsonTextReader = NewtonsoftJson::Newtonsoft.Json.JsonTextReader; namespace PepperDash.Core.JsonToSimpl { @@ -165,7 +170,7 @@ namespace PepperDash.Core.JsonToSimpl #if NET6_0 using (var reader = new JsonTextReader(new System.IO.StringReader(json))) #else - using (var reader = new JsonTextReader(new Crestron.SimplSharp.CrestronIO.StringReader(json))) + using (var reader = new JsonTextReader(new System.IO.StringReader(json))) #endif { var startDepth = reader.Depth; @@ -189,7 +194,7 @@ namespace PepperDash.Core.JsonToSimpl #if NET6_0 using (var reader = new JsonTextReader(new System.IO.StringReader(json))) #else - using (var reader = new JsonTextReader(new Crestron.SimplSharp.CrestronIO.StringReader(json))) + using (var reader = new JsonTextReader(new System.IO.StringReader(json))) #endif { var startDepth = reader.Depth; diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs index e142d1bc..acadc99a 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs @@ -1,9 +1,12 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; -using Newtonsoft.Json.Linq; +using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; +using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; using PepperDash.Core.Config; namespace PepperDash.Core.JsonToSimpl diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 877d8997..49d55eb4 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -1,4 +1,6 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using System.Net; using System.Reflection; @@ -7,7 +9,8 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronDataStore; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronLogger; -using Newtonsoft.Json; +using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; +using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; using PepperDash.Core.Logging; using Serilog; using Serilog.Context; diff --git a/src/PepperDash.Core/Logging/DebugContext.cs b/src/PepperDash.Core/Logging/DebugContext.cs index e90ec86a..cdea6837 100644 --- a/src/PepperDash.Core/Logging/DebugContext.cs +++ b/src/PepperDash.Core/Logging/DebugContext.cs @@ -1,9 +1,12 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; -using Newtonsoft.Json; +using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; +using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; namespace PepperDash.Core diff --git a/src/PepperDash.Core/Logging/DebugMemory.cs b/src/PepperDash.Core/Logging/DebugMemory.cs index 286707b8..327d04b4 100644 --- a/src/PepperDash.Core/Logging/DebugMemory.cs +++ b/src/PepperDash.Core/Logging/DebugMemory.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +extern alias NewtonsoftJson; + +using System.Collections.Generic; using Crestron.SimplSharp; -using Newtonsoft.Json; +using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; namespace PepperDash.Core.Logging { diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 9c3df14e..856abbdb 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -1,4 +1,6 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -16,7 +18,7 @@ using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certi using System.IO; using Org.BouncyCastle.Asn1.X509; using Serilog.Formatting; -using Newtonsoft.Json.Linq; +using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using Serilog.Formatting.Json; namespace PepperDash.Core diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj index daa5c6da..875e6275 100644 --- a/src/PepperDash.Core/PepperDash.Core.csproj +++ b/src/PepperDash.Core/PepperDash.Core.csproj @@ -44,6 +44,9 @@ + + global,NewtonsoftJson + @@ -53,7 +56,9 @@ - + + global,NewtonsoftJson + diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs index 11ec4e2f..42bc555b 100644 --- a/src/PepperDash.Core/Web/WebApiServer.cs +++ b/src/PepperDash.Core/Web/WebApiServer.cs @@ -1,10 +1,13 @@ -using System; +extern alias NewtonsoftJson; + +using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; +using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; +using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using PepperDash.Core.Web.RequestHandlers; namespace PepperDash.Core.Web diff --git a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs index f8c815fd..831d2d02 100644 --- a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs +++ b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs @@ -1,10 +1,12 @@ -using System; +extern alias NewtonsoftJson; + +using System; using Crestron.SimplSharp; // For Basic SIMPL# Classes using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.Net.Http; using Crestron.SimplSharp.Net.Https; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; +using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using PepperDash.Core.JsonToSimpl; diff --git a/src/PepperDash.Essentials.Core/Global/Global.cs b/src/PepperDash.Essentials.Core/Global/Global.cs index 37d4472d..571f5017 100644 --- a/src/PepperDash.Essentials.Core/Global/Global.cs +++ b/src/PepperDash.Essentials.Core/Global/Global.cs @@ -54,9 +54,10 @@ namespace PepperDash.Essentials.Core // TODO: consider making this configurable later /// - /// The CultureInfo for formatting + /// Gets or sets the CultureInfo for the running system. Default is InvariantCulture. This is used for all parsing and formatting in Essentials to ensure consistent behavior regardless of the culture settings of the processor. /// - public static IFormatProvider Culture = CultureInfo.CreateSpecificCulture("en-US"); + public static IFormatProvider Culture = CultureInfo.InvariantCulture; + /// /// True when the processor type is a DMPS variant diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index 251ba316..5087ac20 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -3,7 +3,7 @@ Debug;Release;Debug 4.7.2 - net472 + net6;net8 true true bin\$(Configuration)\ diff --git a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj index 7be4372d..ec3a1d70 100644 --- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj +++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj @@ -3,7 +3,7 @@ Debug;Release;Debug 4.7.2 - net472 + net6;net8 true bin\$(Configuration)\ Essentials Devices Common diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index d13d1a09..1a5a5c0d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -1,7 +1,7 @@  PepperDash.Essentials.AppServer - net472 + net6;net8 mobile-control-messengers mobile-control-messengers mobile-control-messengers diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index 235e0899..f7a7b016 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -1,7 +1,7 @@  PepperDash.Essentials - net472 + net6;net8 true false epi-essentials-mobile-control diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 6fbcf06c..608c362f 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO.Compression; using System.Linq; using System.Reflection; @@ -12,7 +12,6 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Routing; -using PepperDash.Essentials.Core.Web; using Serilog.Events; namespace PepperDash.Essentials @@ -33,12 +32,39 @@ namespace PepperDash.Essentials /// public ControlSystem() : base() + { + Thread.MaxNumberOfUserThreads = 400; Global.ControlSystem = this; DeviceManager.Initialize(this); SecretsManager.Initialize(); SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true; + + Debug.SetErrorLogMinimumDebugLevel(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? LogEventLevel.Warning : LogEventLevel.Verbose); + + // AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; + } + + private System.Reflection.Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) + { + var assemblyName = new System.Reflection.AssemblyName(args.Name).Name; + if (assemblyName == "PepperDash_Core") + { + return System.Reflection.Assembly.LoadFrom("PepperDashCore.dll"); + } + + if (assemblyName == "PepperDash_Essentials_Core") + { + return System.Reflection.Assembly.LoadFrom("PepperDash.Essentials.Core.dll"); + } + + if (assemblyName == "Essentials Devices Common") + { + return System.Reflection.Assembly.LoadFrom("PepperDash.Essentials.Devices.Common.dll"); + } + + return null; } /// @@ -75,6 +101,9 @@ namespace PepperDash.Essentials { DeterminePlatform(); + // Print .NET runtime version + Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); + if (Debug.DoNotLoadConfigOnNextBoot) { CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file", @@ -123,7 +152,7 @@ namespace PepperDash.Essentials CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); - DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); + //DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); if (!Debug.DoNotLoadConfigOnNextBoot) { diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index 20a42ffd..9150191d 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -6,9 +6,9 @@ PepperDash.Essentials PepperDashEssentials - net472 + net6;net8 true - bin\$(Configuration)\ + $(ProjectDir)bin\$(Configuration)\ PepperDash Essentials PepperDashEssentials $(Version) From bdd398e2e6e6a0cc78672daf9dfa660087d52e98 Mon Sep 17 00:00:00 2001 From: jtalborough Date: Wed, 26 Feb 2025 10:39:09 -0500 Subject: [PATCH 003/161] feat: implement WebSocket classes and update culture settings; bump PepperDashCore version --- runtimeconfig.json | 7 ++++ .../Global/Global.cs | 16 +++++-- .../SomeWebSocketClass.cs | 37 ++++++++++++++++ .../Web/DebugWebsocketSink.cs | 0 .../DebugSessionRequestHandler.cs | 14 ++++--- .../Web/RequestHandlers/DebugWebsocketSink.cs | 42 +++++++++++++++++++ .../SomeOtherWebSocketClass.cs | 37 ++++++++++++++++ ...lsHuddleSpaceFusionSystemControllerBase.cs | 40 ++++++++++++++++++ 8 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 runtimeconfig.json create mode 100644 src/PepperDash.Essentials.Core/SomeWebSocketClass.cs create mode 100644 src/PepperDash.Essentials.Core/Web/DebugWebsocketSink.cs create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugWebsocketSink.cs create mode 100644 src/PepperDash.Essentials.Devices.Common/SomeOtherWebSocketClass.cs create mode 100644 src/PepperDash.Essentials/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs diff --git a/runtimeconfig.json b/runtimeconfig.json new file mode 100644 index 00000000..32835bac --- /dev/null +++ b/runtimeconfig.json @@ -0,0 +1,7 @@ +{ + "runtimeOptions": { + "configProperties": { + "System.Globalization.Invariant": false + } + } +} diff --git a/src/PepperDash.Essentials.Core/Global/Global.cs b/src/PepperDash.Essentials.Core/Global/Global.cs index 571f5017..d1ed3efb 100644 --- a/src/PepperDash.Essentials.Core/Global/Global.cs +++ b/src/PepperDash.Essentials.Core/Global/Global.cs @@ -1,6 +1,4 @@ - - -using System; +using System; using System.Linq; using System.Text.RegularExpressions; using System.Globalization; @@ -311,6 +309,18 @@ namespace PepperDash.Essentials.Core CrestronConsole.PrintLine("Error starting CrestronDataStoreStatic: {0}", err); return; } + + try + { + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture("en"); + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture("en"); + } + catch (CultureNotFoundException) + { + // If specific culture fails, fall back to invariant + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; + } } } diff --git a/src/PepperDash.Essentials.Core/SomeWebSocketClass.cs b/src/PepperDash.Essentials.Core/SomeWebSocketClass.cs new file mode 100644 index 00000000..b87e87ae --- /dev/null +++ b/src/PepperDash.Essentials.Core/SomeWebSocketClass.cs @@ -0,0 +1,37 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +public class SomeWebSocketClass +{ + private ClientWebSocket _webSocket; + + public SomeWebSocketClass() + { + _webSocket = new ClientWebSocket(); + } + + public async Task ConnectAsync(Uri uri) + { + await _webSocket.ConnectAsync(uri, CancellationToken.None); + } + + public async Task SendAsync(string message) + { + var buffer = System.Text.Encoding.UTF8.GetBytes(message); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + + public async Task ReceiveAsync() + { + var buffer = new byte[1024]; + var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); + } + + public async Task CloseAsync() + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } +} diff --git a/src/PepperDash.Essentials.Core/Web/DebugWebsocketSink.cs b/src/PepperDash.Essentials.Core/Web/DebugWebsocketSink.cs new file mode 100644 index 00000000..e69de29b diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs index 01e38834..56e2b880 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using PepperDash.Essentials.Core.Web.RequestHandlers; namespace PepperDash.Essentials.Core.Web.RequestHandlers { @@ -18,6 +19,9 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// public class DebugSessionRequestHandler : WebApiBaseRequestHandler { + + private readonly DebugWebsocketSink _sink = new DebugWebsocketSink(); + /// /// Constructor /// @@ -49,21 +53,21 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers var port = 0; - if (!Debug.WebsocketSink.IsRunning) + if (!_sink.IsRunning) { Debug.LogMessage(LogEventLevel.Information, "Starting WS Server"); // Generate a random port within a specified range port = new Random().Next(65435, 65535); // Start the WS Server - Debug.WebsocketSink.StartServerAndSetPort(port); + _sink.StartServerAndSetPort(port); Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); } - var url = Debug.WebsocketSink.Url; + var url = _sink.Url; object data = new { - url = Debug.WebsocketSink.Url + url = _sink.Url }; Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url); @@ -90,7 +94,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// protected override void HandlePost(HttpCwsContext context) { - Debug.WebsocketSink.StopServer(); + _sink.StopServer(); context.Response.StatusCode = 200; context.Response.StatusDescription = "OK"; diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugWebsocketSink.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugWebsocketSink.cs new file mode 100644 index 00000000..add30634 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugWebsocketSink.cs @@ -0,0 +1,42 @@ +using System; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers +{ + public class DebugWebsocketSink + { + private bool _isRunning; + private string _url; + + public bool IsRunning => _isRunning; + public string Url => _url; + + public void StartServerAndSetPort(int port) + { + try + { + _url = $"ws://localhost:{port}"; + _isRunning = true; + // Implement actual server startup logic here + } + catch (Exception ex) + { + _isRunning = false; + throw new Exception($"Failed to start debug websocket server: {ex.Message}"); + } + } + + public void StopServer() + { + try + { + // Implement actual server shutdown logic here + _isRunning = false; + _url = null; + } + catch (Exception ex) + { + throw new Exception($"Failed to stop debug websocket server: {ex.Message}"); + } + } + } +} diff --git a/src/PepperDash.Essentials.Devices.Common/SomeOtherWebSocketClass.cs b/src/PepperDash.Essentials.Devices.Common/SomeOtherWebSocketClass.cs new file mode 100644 index 00000000..675775a4 --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/SomeOtherWebSocketClass.cs @@ -0,0 +1,37 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +public class SomeOtherWebSocketClass +{ + private ClientWebSocket _webSocket; + + public SomeOtherWebSocketClass() + { + _webSocket = new ClientWebSocket(); + } + + public async Task ConnectAsync(Uri uri) + { + await _webSocket.ConnectAsync(uri, CancellationToken.None); + } + + public async Task SendAsync(string message) + { + var buffer = System.Text.Encoding.UTF8.GetBytes(message); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + + public async Task ReceiveAsync() + { + var buffer = new byte[1024]; + var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); + } + + public async Task CloseAsync() + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } +} diff --git a/src/PepperDash.Essentials/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs b/src/PepperDash.Essentials/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs new file mode 100644 index 00000000..9c78c4aa --- /dev/null +++ b/src/PepperDash.Essentials/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs @@ -0,0 +1,40 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace PepperDash.Essentials.Fusion +{ + public class EssentialsHuddleSpaceFusionSystemControllerBase + { + private ClientWebSocket _webSocket; + + public EssentialsHuddleSpaceFusionSystemControllerBase() + { + _webSocket = new ClientWebSocket(); + } + + public async Task ConnectAsync(Uri uri) + { + await _webSocket.ConnectAsync(uri, CancellationToken.None); + } + + public async Task SendAsync(string message) + { + var buffer = System.Text.Encoding.UTF8.GetBytes(message); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + + public async Task ReceiveAsync() + { + var buffer = new byte[1024]; + var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); + } + + public async Task CloseAsync() + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + } +} From 4a2c9fe31188e6fa4948e4d472c81b08d291ad2a Mon Sep 17 00:00:00 2001 From: jtalborough Date: Wed, 26 Feb 2025 16:27:44 -0500 Subject: [PATCH 004/161] fix: update target framework to net8 and bump PepperDashCore version to 2.0.0-alpha-462 BREAKING CHANGE: Target Framework is now .NET 8: --- .../PepperDash.Essentials.Core.csproj | 2 +- .../PepperDash.Essentials.Devices.Common.csproj | 2 +- src/PepperDash.Essentials/PepperDash.Essentials.csproj | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index 5087ac20..a474d8c3 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -3,7 +3,7 @@ Debug;Release;Debug 4.7.2 - net6;net8 + net8 true true bin\$(Configuration)\ diff --git a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj index ec3a1d70..d1776f73 100644 --- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj +++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj @@ -3,7 +3,7 @@ Debug;Release;Debug 4.7.2 - net6;net8 + net8 true bin\$(Configuration)\ Essentials Devices Common diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index 9150191d..c8a4dc7e 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -6,7 +6,7 @@ PepperDash.Essentials PepperDashEssentials - net6;net8 + net8 true $(ProjectDir)bin\$(Configuration)\ PepperDash Essentials @@ -14,6 +14,9 @@ $(Version) false + + Program + full From fbcd9c84dac6a1f63cbec4774630bef373481880 Mon Sep 17 00:00:00 2001 From: jtalborough Date: Thu, 27 Feb 2025 17:12:57 -0500 Subject: [PATCH 005/161] wip: address performance issues in plugin loading and versioning --- .../Plugins/PluginLoader.cs | 418 ++++++++++++++++-- ...Essentials.MobileControl.Messengers.csproj | 2 +- ...PepperDash.Essentials.MobileControl.csproj | 2 +- 3 files changed, 377 insertions(+), 45 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index 2f195ba9..17619b2f 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -5,6 +5,11 @@ using System.Linq; using System.Reflection; using Crestron.SimplSharp; using Newtonsoft.Json; +using System.Reflection.PortableExecutable; +using System.Reflection.Metadata; +using SystemIO = System.IO; +using CrestronIO = System.IO; + using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; @@ -26,6 +31,11 @@ namespace PepperDash.Essentials /// static List LoadedPluginFolderAssemblies; + /// + /// List of plugins that were found to be incompatible with .NET 8 + /// + public static List IncompatiblePlugins { get; private set; } + /// /// The assembly for the Essentials Framework /// @@ -54,12 +64,28 @@ namespace PepperDash.Essentials // The temp directory where .cplz archives will be unzipped to static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp"; + // Known incompatible types in .NET 8 + private static readonly HashSet KnownIncompatibleTypes = new HashSet + { + "System.Net.ICertificatePolicy", + "System.Security.Cryptography.SHA1CryptoServiceProvider", + "System.Web.HttpUtility", + "System.Configuration.ConfigurationManager", + "System.Web.Services.Protocols.SoapHttpClientProtocol", + "System.Runtime.Remoting", + "System.EnterpriseServices", + "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter", + "System.Security.SecurityManager", + "System.Security.Permissions.FileIOPermission", + "System.AppDomain.CreateDomain" + }; static PluginLoader() { LoadedAssemblies = new List(); LoadedPluginFolderAssemblies = new List(); EssentialsPluginAssemblies = new List(); + IncompatiblePlugins = new List(); } /// @@ -69,7 +95,7 @@ namespace PepperDash.Essentials { Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials"); // Get the loaded assembly filenames - var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); + var appDi = new SystemIO.DirectoryInfo(Global.ApplicationDirectoryPathPrefix); var assemblyFiles = appDi.GetFiles("*.dll"); Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length); @@ -136,19 +162,108 @@ namespace PepperDash.Essentials } /// - /// Loads an assembly via Reflection and adds it to the list of loaded assemblies + /// Checks if a plugin is compatible with .NET 8 by examining its metadata /// - /// - static LoadedAssembly LoadAssembly(string filePath) + /// Path to the plugin assembly + /// Tuple with compatibility result, reason if incompatible, and referenced assemblies + public static (bool IsCompatible, string Reason, List References) IsPluginCompatibleWithNet8(string filePath) { try { - //Debug.LogMessage(LogEventLevel.Verbose, "Attempting to load {0}", filePath); + List referencedAssemblies = new List(); + + using (SystemIO.FileStream fs = new SystemIO.FileStream(filePath, SystemIO.FileMode.Open, + SystemIO.FileAccess.Read, SystemIO.FileShare.ReadWrite)) + using (PEReader peReader = new PEReader(fs)) + { + if (!peReader.HasMetadata) + return (false, "Not a valid .NET assembly", referencedAssemblies); + + MetadataReader metadataReader = peReader.GetMetadataReader(); + + // Collect assembly references + foreach (var assemblyRefHandle in metadataReader.AssemblyReferences) + { + var assemblyRef = metadataReader.GetAssemblyReference(assemblyRefHandle); + string assemblyName = metadataReader.GetString(assemblyRef.Name); + referencedAssemblies.Add(assemblyName); + } + + // Check for references to known incompatible types + foreach (var typeRefHandle in metadataReader.TypeReferences) + { + var typeRef = metadataReader.GetTypeReference(typeRefHandle); + string typeNamespace = metadataReader.GetString(typeRef.Namespace); + string typeName = metadataReader.GetString(typeRef.Name); + + string fullTypeName = $"{typeNamespace}.{typeName}"; + if (KnownIncompatibleTypes.Contains(fullTypeName)) + { + return (false, $"Uses incompatible type: {fullTypeName}", referencedAssemblies); + } + } + + // Check for explicit .NET 8 compatibility attribute + bool hasNet8Attribute = false; + foreach (var customAttributeHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes()) + { + var customAttribute = metadataReader.GetCustomAttribute(customAttributeHandle); + var ctorHandle = customAttribute.Constructor; + + if (ctorHandle.Kind == HandleKind.MemberReference) + { + var memberRef = metadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle); + var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)memberRef.Parent); + + string typeName = metadataReader.GetString(typeRef.Name); + if (typeName == "Net8CompatibleAttribute" || typeName == "TargetFrameworkAttribute") + { + hasNet8Attribute = true; + break; + } + } + } + + if (hasNet8Attribute) + { + return (true, null, referencedAssemblies); + } + + // If we can't determine incompatibility, assume it's compatible + return (true, null, referencedAssemblies); + } + } + catch (Exception ex) + { + return (false, $"Error analyzing assembly: {ex.Message}", new List()); + } + } + + /// + /// Loads an assembly via Reflection and adds it to the list of loaded assemblies + /// + /// Path to the assembly file + /// Name of the plugin requesting this assembly (null for direct loads) + static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null) + { + try + { + // Check .NET 8 compatibility before loading + var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath); + if (!isCompatible) + { + string fileName = CrestronIO.Path.GetFileName(filePath); + Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); + + var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy); + IncompatiblePlugins.Add(incompatiblePlugin); + return null; + } + var assembly = Assembly.LoadFrom(filePath); if (assembly != null) { var assyVersion = GetAssemblyVersion(assembly); - var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly); LoadedAssemblies.Add(loadedAssembly); Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version); @@ -158,15 +273,47 @@ namespace PepperDash.Essentials { Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath); } - return null; } - catch (Exception ex) + catch(SystemIO.FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) { - Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath); + // Get the assembly name from the file path + string assemblyName = CrestronIO.Path.GetFileNameWithoutExtension(filePath); + + // Try to find the already loaded assembly + var existingAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.OrdinalIgnoreCase)); + + if (existingAssembly != null) + { + Debug.LogMessage(LogEventLevel.Information, "Assembly '{0}' is already loaded, using existing instance", assemblyName); + var assyVersion = GetAssemblyVersion(existingAssembly); + var loadedAssembly = new LoadedAssembly(existingAssembly.GetName().Name, assyVersion, existingAssembly); + LoadedAssemblies.Add(loadedAssembly); + return loadedAssembly; + } + + Debug.LogMessage(LogEventLevel.Warning, "Assembly with same name already loaded but couldn't find it: {0}", filePath); + return null; + } + catch(Exception ex) + { + string fileName = CrestronIO.Path.GetFileName(filePath); + + // Check if this might be a .NET Framework compatibility issue + if (ex.Message.Contains("Could not load type") || + ex.Message.Contains("Unable to load one or more of the requested types")) + { + Debug.LogMessage(LogEventLevel.Error, "Error loading assembly {0}: Likely .NET 8 compatibility issue: {1}", + fileName, ex.Message); + IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, ex.Message, requestedBy)); + } + else + { + Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath); + } return null; } - } /// @@ -242,11 +389,23 @@ namespace PepperDash.Essentials CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version); } - //CrestronConsole.ConsoleCommandResponse("Loaded Assemblies:" + CrestronEnvironment.NewLine); - //foreach (var assembly in LoadedAssemblies) - //{ - // CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version); - //} + if (IncompatiblePlugins.Count > 0) + { + CrestronConsole.ConsoleCommandResponse("Incompatible Plugins:" + CrestronEnvironment.NewLine); + foreach (var plugin in IncompatiblePlugins) + { + if (plugin.TriggeredBy != "Direct load") + { + CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine, + plugin.Name, plugin.Reason, plugin.TriggeredBy); + } + else + { + CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine, + plugin.Name, plugin.Reason); + } + } + } } /// @@ -256,14 +415,14 @@ namespace PepperDash.Essentials { Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder..."); - var pluginDi = new DirectoryInfo(_pluginDirectory); + var pluginDi = new SystemIO.DirectoryInfo(_pluginDirectory); var pluginFiles = pluginDi.GetFiles("*.dll"); if (pluginFiles.Length > 0) { - if (!Directory.Exists(_loadedPluginsDirectoryPath)) + if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath)) { - Directory.CreateDirectory(_loadedPluginsDirectoryPath); + SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath); } } @@ -280,14 +439,14 @@ namespace PepperDash.Essentials filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name; // Check if there is a previous file in the loadedPlugins directory and delete - if (File.Exists(filePath)) + if (SystemIO.File.Exists(filePath)) { Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath); - File.Delete(filePath); + SystemIO.File.Delete(filePath); } // Move the file - File.Move(pluginFile.FullName, filePath); + SystemIO.File.Move(pluginFile.FullName, filePath); Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath); } else @@ -310,9 +469,9 @@ namespace PepperDash.Essentials /// static void UnzipAndMoveCplzArchives() { - Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from user folder..."); - //var di = new DirectoryInfo(_pluginDirectory); - //var zFiles = di.GetFiles("*.cplz"); + Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder..."); + var di = new SystemIO.DirectoryInfo(_pluginDirectory); + var zFiles = di.GetFiles("*.cplz"); //// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users @@ -326,16 +485,16 @@ namespace PepperDash.Essentials if (cplzFiles.Length > 0) { - if (!Directory.Exists(_loadedPluginsDirectoryPath)) + if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath)) { - Directory.CreateDirectory(_loadedPluginsDirectoryPath); + SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath); } } foreach (var zfi in cplzFiles) { - Directory.CreateDirectory(_tempDirectory); - var tempDi = new DirectoryInfo(_tempDirectory); + SystemIO.Directory.CreateDirectory(_tempDirectory); + var tempDi = new SystemIO.DirectoryInfo(_tempDirectory); Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName); var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName); @@ -353,14 +512,14 @@ namespace PepperDash.Essentials filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name; // Check if there is a previous file in the loadedPlugins directory and delete - if (File.Exists(filePath)) + if (SystemIO.File.Exists(filePath)) { Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath); - File.Delete(filePath); + SystemIO.File.Delete(filePath); } // Move the file - File.Move(tempFile.FullName, filePath); + SystemIO.File.Move(tempFile.FullName, filePath); Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath); } else @@ -376,7 +535,7 @@ namespace PepperDash.Essentials } // Delete the .cplz and the temp directory - Directory.Delete(_tempDirectory, true); + SystemIO.Directory.Delete(_tempDirectory, true); zfi.Delete(); } @@ -389,16 +548,40 @@ namespace PepperDash.Essentials static void LoadPluginAssemblies() { Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder..."); - var pluginDi = new DirectoryInfo(_loadedPluginsDirectoryPath); + var pluginDi = new CrestronIO.DirectoryInfo(_loadedPluginsDirectoryPath); var pluginFiles = pluginDi.GetFiles("*.dll"); Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length); + // First, check compatibility of all assemblies before loading any + var assemblyCompatibility = new Dictionary References)>(); + foreach (var pluginFile in pluginFiles) { - var loadedAssembly = LoadAssembly(pluginFile.FullName); - - LoadedPluginFolderAssemblies.Add(loadedAssembly); + string fileName = pluginFile.Name; + assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName); + } + + // Now load compatible assemblies and track incompatible ones + foreach (var pluginFile in pluginFiles) + { + string fileName = pluginFile.Name; + var (isCompatible, reason, references) = assemblyCompatibility[fileName]; + + if (!isCompatible) + { + Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); + IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, reason, null)); + continue; + } + + // Try to load the assembly + var loadedAssembly = LoadAssembly(pluginFile.FullName, null); + + if (loadedAssembly != null) + { + LoadedPluginFolderAssemblies.Add(loadedAssembly); + } } Debug.LogMessage(LogEventLevel.Information, "All Plugins Loaded."); @@ -410,8 +593,13 @@ namespace PepperDash.Essentials static void LoadCustomPluginTypes() { Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types..."); + foreach (var loadedAssembly in LoadedPluginFolderAssemblies) { + // Skip if assembly is null (can happen if we had loading issues) + if (loadedAssembly == null || loadedAssembly.Assembly == null) + continue; + // iteratate this assembly's classes, looking for "LoadPlugin()" methods try { @@ -422,11 +610,49 @@ namespace PepperDash.Essentials types = assy.GetTypes(); Debug.LogMessage(LogEventLevel.Debug, $"Got types for assembly {assy.GetName().Name}"); } + catch (ReflectionTypeLoadException e) + { + Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", + loadedAssembly.Name, e.Message); + + // Check if any of the loader exceptions are due to missing assemblies + foreach (var loaderEx in e.LoaderExceptions) + { + if (loaderEx is SystemIO.FileNotFoundException fileNotFoundEx) + { + string missingAssembly = fileNotFoundEx.FileName; + if (!string.IsNullOrEmpty(missingAssembly)) + { + Debug.LogMessage(LogEventLevel.Warning, "Assembly {0} requires missing dependency: {1}", + loadedAssembly.Name, missingAssembly); + + // Add to incompatible plugins with dependency information + IncompatiblePlugins.Add(new IncompatiblePlugin( + CrestronIO.Path.GetFileName(missingAssembly), + $"Missing dependency required by {loadedAssembly.Name}", + loadedAssembly.Name)); + } + } + } + + Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); + continue; + } catch (TypeLoadException e) { Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", loadedAssembly.Name, e.Message); Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); + + // Add to incompatible plugins if this is likely a .NET 8 compatibility issue + if (e.Message.Contains("Could not load type") || + e.Message.Contains("Unable to load one or more of the requested types")) + { + IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, + $"Type loading error: {e.Message}", + null)); + } + continue; } @@ -451,25 +677,65 @@ namespace PepperDash.Essentials loadedAssembly.Name, e.Message, type.Name); continue; } - } } catch (Exception e) { Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}", - loadedAssembly.Name, e.Message); + loadedAssembly.Name, e.Message); Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace); + + // Add to incompatible plugins if this is likely a .NET 8 compatibility issue + if (e.Message.Contains("Could not load type") || + e.Message.Contains("Unable to load one or more of the requested types")) + { + IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, + $"Assembly loading error: {e.Message}", + null)); + } + continue; } } + + // Update incompatible plugins with dependency information + UpdateIncompatiblePluginDependencies(new Dictionary>()); + // plugin dll will be loaded. Any classes in plugin should have a static constructor // that registers that class with the Core.DeviceFactory Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types."); } /// - /// Loads a custom plugin and performs a dependency check to ensure compatibility with the required Essentials - /// framework version. + /// Updates incompatible plugins with information about which plugins depend on them + /// + private static void UpdateIncompatiblePluginDependencies(Dictionary> pluginDependencies) + { + // For each incompatible plugin + foreach (var incompatiblePlugin in IncompatiblePlugins) + { + // If it already has a requestedBy, skip it + if (incompatiblePlugin.TriggeredBy != "Direct load") + continue; + + // Find plugins that depend on this incompatible plugin + foreach (var plugin in pluginDependencies) + { + string pluginName = plugin.Key; + List dependencies = plugin.Value; + + // If this plugin depends on the incompatible plugin + if (dependencies.Contains(incompatiblePlugin.Name) || + dependencies.Any(d => d.StartsWith(incompatiblePlugin.Name + ","))) + { + incompatiblePlugin.UpdateTriggeringPlugin(pluginName); + break; + } + } + } + } + /// + /// Loads a /// /// This method verifies that the plugin meets the minimum required Essentials framework version /// before loading it. If the plugin fails the dependency check, it is skipped, and a log message is generated. If @@ -531,7 +797,7 @@ namespace PepperDash.Essentials { Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory); - if (Directory.Exists(_pluginDirectory)) + if (SystemIO.Directory.Exists(_pluginDirectory)) { Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins"); @@ -541,7 +807,7 @@ namespace PepperDash.Essentials // Deal with any .cplz files UnzipAndMoveCplzArchives(); - if (Directory.Exists(_loadedPluginsDirectoryPath)) + if (SystemIO.Directory.Exists(_loadedPluginsDirectoryPath)) { // Load the assemblies from the loadedPlugins folder into the AppDomain LoadPluginAssemblies(); @@ -549,9 +815,27 @@ namespace PepperDash.Essentials // Load the types from any custom plugin assemblies LoadCustomPluginTypes(); } + + // Report on incompatible plugins + if (IncompatiblePlugins.Count > 0) + { + Debug.LogMessage(LogEventLevel.Warning, "Found {0} incompatible plugins:", IncompatiblePlugins.Count); + foreach (var plugin in IncompatiblePlugins) + { + if (plugin.TriggeredBy != "Direct load") + { + Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})", + plugin.Name, plugin.Reason, plugin.TriggeredBy); + } + else + { + Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}", + plugin.Name, plugin.Reason); + } + } + } } } - } /// @@ -598,4 +882,52 @@ namespace PepperDash.Essentials Assembly = assembly; } } + + /// + /// Represents a plugin that was found to be incompatible with .NET 8 + /// + public class IncompatiblePlugin + { + [JsonProperty("name")] + public string Name { get; private set; } + + [JsonProperty("reason")] + public string Reason { get; private set; } + + [JsonProperty("triggeredBy")] + public string TriggeredBy { get; private set; } + + public IncompatiblePlugin(string name, string reason, string triggeredBy = null) + { + Name = name; + Reason = reason; + TriggeredBy = triggeredBy ?? "Direct load"; + } + + /// + /// Updates the plugin that triggered this incompatibility + /// + /// Name of the plugin that requires this incompatible plugin + public void UpdateTriggeringPlugin(string triggeringPlugin) + { + if (!string.IsNullOrEmpty(triggeringPlugin)) + { + TriggeredBy = triggeringPlugin; + } + } + } + + /// + /// Attribute to explicitly mark a plugin as .NET 8 compatible + /// + [AttributeUsage(AttributeTargets.Assembly)] + public class Net8CompatibleAttribute : Attribute + { + public bool IsCompatible { get; } + + public Net8CompatibleAttribute(bool isCompatible = true) + { + IsCompatible = isCompatible; + } + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index 1a5a5c0d..7eaefc16 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -1,7 +1,7 @@  PepperDash.Essentials.AppServer - net6;net8 + net8 mobile-control-messengers mobile-control-messengers mobile-control-messengers diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index f7a7b016..73d648f7 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -1,7 +1,7 @@  PepperDash.Essentials - net6;net8 + net8 true false epi-essentials-mobile-control From 790b5a6fb956fa42cdea7a42f747de6781a2b02e Mon Sep 17 00:00:00 2001 From: jtalborough Date: Mon, 3 Mar 2025 18:16:14 -0500 Subject: [PATCH 006/161] fix: update condition for CPZ copy target and remove obsolete workflows --- .github/workflows/essentials-3-dev-build.yml | 241 +++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 .github/workflows/essentials-3-dev-build.yml diff --git a/.github/workflows/essentials-3-dev-build.yml b/.github/workflows/essentials-3-dev-build.yml new file mode 100644 index 00000000..a870d2cc --- /dev/null +++ b/.github/workflows/essentials-3-dev-build.yml @@ -0,0 +1,241 @@ +name: Essentials v3 Development Build + +on: + push: + branches: + - feature-3.0.0/* + - hotfix-3.0.0/* + - release-3.0.0/* + - development-3.0.0 + +env: + SOLUTION_PATH: . + SOLUTION_FILE: PepperDash.Essentials + VERSION: 0.0.0-buildtype-buildnumber + BUILD_TYPE: Debug + RELEASE_BRANCH: main +jobs: + Build_Project_4-Series: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + # Detect environment (Act vs GitHub) + - name: Detect environment + id: detect_env + run: | + if [ -n "$ACT" ]; then + echo "is_local=true" >> $GITHUB_OUTPUT + else + echo "is_local=false" >> $GITHUB_OUTPUT + fi + + # Install all prerequisites + - name: Install prerequisites + run: | + apt-get update + apt-get install -y curl wget libicu-dev git unzip + + - name: Set Version Number + id: setVersion + shell: bash + run: | + latestVersion="3.0.0" + newVersion=$latestVersion + phase="" + newVersionString="" + + if [[ $GITHUB_REF =~ ^refs/pull/.* ]]; then + phase="beta" + newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}" + elif [[ $GITHUB_REF =~ ^refs/heads/hotfix-3.0.0/.* ]]; then + phase="hotfix" + newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}" + elif [[ $GITHUB_REF =~ ^refs/heads/feature-3.0.0/.* ]]; then + phase="alpha" + newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}" + elif [[ $GITHUB_REF == "refs/heads/development-3.0.0" ]]; then + phase="beta" + newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}" + elif [[ $GITHUB_REF =~ ^refs/heads/release-3.0.0/.* ]]; then + version=$(echo $GITHUB_REF | awk -F '/' '{print $NF}' | sed 's/v//') + phase="rc" + newVersionString="${version}-${phase}-${GITHUB_RUN_NUMBER}" + else + # For local builds or unrecognized branches + newVersionString="${newVersion}-local" + fi + + echo "version=$newVersionString" >> $GITHUB_OUTPUT + + # Create Build Properties file + - name: Create Build Properties + run: | + cat > Directory.Build.props << EOF + + + ${{ steps.setVersion.outputs.version }} + ${{ steps.setVersion.outputs.version }} + ${{ steps.setVersion.outputs.version }} + ${{ steps.setVersion.outputs.version }} + ${{ steps.setVersion.outputs.version }} + ${{ steps.setVersion.outputs.version }} + + + EOF + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + + - name: Restore NuGet Packages + run: dotnet restore ${SOLUTION_FILE}.sln + + - name: Build Solution + run: dotnet build ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --no-restore + + # Copy the CPZ file to the output directory with version in the filename + - name: Copy and Rename CPZ Files + run: | + mkdir -p ./output/cpz + + # Find the main CPZ file in the build output + if [ -f "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" ]; then + cp "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" "./output/cpz/PepperDashEssentials.${{ steps.setVersion.outputs.version }}.cpz" + echo "Main CPZ file copied and renamed successfully." + else + echo "Warning: Main CPZ file not found at expected location." + find ./src -name "*.cpz" | xargs -I {} cp {} ./output/cpz/ + fi + + - name: Pack Solution + run: dotnet pack ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --output ./output/nuget --no-build + + # List build artifacts (runs in both environments) + - name: List Build Artifacts + run: | + echo "=== Build Artifacts ===" + echo "NuGet Packages:" + find ./output/nuget -type f | sort + echo "" + echo "CPZ/CPLZ Files:" + find ./output -name "*.cpz" -o -name "*.cplz" | sort + echo "=======================" + + # Enhanced package inspection for local runs + - name: Inspect NuGet Packages + if: steps.detect_env.outputs.is_local == 'true' + run: | + echo "=== NuGet Package Details ===" + for pkg in $(find ./output/nuget -name "*.nupkg"); do + echo "Package: $(basename "$pkg")" + echo "Size: $(du -h "$pkg" | cut -f1)" + + # Extract and show package contents + echo "Contents:" + unzip -l "$pkg" | tail -n +4 | head -n -2 + echo "--------------------------" + + # Try to extract and show the nuspec file (contains metadata) + echo "Metadata:" + unzip -p "$pkg" "*.nuspec" 2>/dev/null | grep -E "(||||)" || echo "Metadata extraction failed" + echo "--------------------------" + done + echo "===========================" + + # Tag creation - GitHub version + - name: Create tag for non-rc builds (GitHub) + if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'false' }} + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git tag ${{ steps.setVersion.outputs.version }} + git push --tags origin + + # Tag creation - Act mock version + - name: Create tag for non-rc builds (Act Mock) + if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'true' }} + run: | + echo "Would create git tag: ${{ steps.setVersion.outputs.version }}" + echo "Would push tag to: origin" + + # Release creation - GitHub version + - name: Create Release (GitHub) + if: steps.detect_env.outputs.is_local == 'false' + id: create_release + uses: ncipollo/release-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + artifacts: 'output/cpz/*,output/**/*.cplz' + generateReleaseNotes: true + prerelease: ${{contains('debug', env.BUILD_TYPE)}} + tag: ${{ steps.setVersion.outputs.version }} + + # Release creation - Act mock version with enhanced output + - name: Create Release (Act Mock) + if: steps.detect_env.outputs.is_local == 'true' + run: | + echo "=== Mock Release Creation ===" + echo "Would create release with:" + echo "- Tag: ${{ steps.setVersion.outputs.version }}" + echo "- Prerelease: ${{contains('debug', env.BUILD_TYPE)}}" + echo "- Artifacts matching pattern: output/cpz/*,output/**/*.cplz" + echo "" + echo "Matching artifacts:" + find ./output/cpz -type f + find ./output -name "*.cplz" + + # Detailed info about release artifacts + echo "" + echo "Artifact Details:" + for artifact in $(find ./output/cpz -type f; find ./output -name "*.cplz"); do + echo "File: $(basename "$artifact")" + echo "Size: $(du -h "$artifact" | cut -f1)" + echo "Created: $(stat -c %y "$artifact")" + echo "MD5: $(md5sum "$artifact" | cut -d' ' -f1)" + echo "--------------------------" + done + echo "============================" + + # NuGet setup - GitHub version + - name: Setup NuGet (GitHub) + if: steps.detect_env.outputs.is_local == 'false' + run: | + dotnet nuget add source https://nuget.pkg.github.com/pepperdash/index.json -n github -u pepperdash -p ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text + + # NuGet setup - Act mock version + - name: Setup NuGet (Act Mock) + if: steps.detect_env.outputs.is_local == 'true' + run: | + echo "=== Mock NuGet Setup ===" + echo "Would add GitHub NuGet source: https://nuget.pkg.github.com/pepperdash/index.json" + echo "=======================" + + # Publish to NuGet - GitHub version + - name: Publish to Nuget (GitHub) + if: steps.detect_env.outputs.is_local == 'false' + run: dotnet nuget push ./output/nuget/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + + # Publish to NuGet - Act mock version + - name: Publish to Nuget (Act Mock) + if: steps.detect_env.outputs.is_local == 'true' + run: | + echo "=== Mock Publish to NuGet ===" + echo "Would publish the following packages to https://api.nuget.org/v3/index.json:" + find ./output/nuget -name "*.nupkg" | sort + echo "=============================" + + # Publish to GitHub NuGet - GitHub version + - name: Publish to Github Nuget (GitHub) + if: steps.detect_env.outputs.is_local == 'false' + run: dotnet nuget push ./output/nuget/*.nupkg --source github --api-key ${{ secrets.GITHUB_TOKEN }} + + # Publish to GitHub NuGet - Act mock version + - name: Publish to Github Nuget (Act Mock) + if: steps.detect_env.outputs.is_local == 'true' + run: | + echo "=== Mock Publish to GitHub NuGet ===" + echo "Would publish the following packages to the GitHub NuGet registry:" + find ./output/nuget -name "*.nupkg" | sort + echo "==================================" \ No newline at end of file From 7bd3ccd54bca5e1149227803cce9e0cc3bf86cf3 Mon Sep 17 00:00:00 2001 From: jtalborough Date: Mon, 3 Mar 2025 18:20:33 -0500 Subject: [PATCH 007/161] fix: adjust installation steps for prerequisites based on environment detection --- .github/workflows/essentials-3-dev-build.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/essentials-3-dev-build.yml b/.github/workflows/essentials-3-dev-build.yml index a870d2cc..3943d5be 100644 --- a/.github/workflows/essentials-3-dev-build.yml +++ b/.github/workflows/essentials-3-dev-build.yml @@ -30,11 +30,17 @@ jobs: echo "is_local=false" >> $GITHUB_OUTPUT fi - # Install all prerequisites - name: Install prerequisites run: | - apt-get update - apt-get install -y curl wget libicu-dev git unzip + if [ "${{ steps.detect_env.outputs.is_local }}" == "true" ]; then + # For Act - no sudo needed + apt-get update + apt-get install -y curl wget libicu-dev git unzip + else + # For GitHub runners - sudo required + sudo apt-get update + sudo apt-get install -y curl wget libicu-dev git unzip + fi - name: Set Version Number id: setVersion From 4e5b8f3897c9717999839cd87fc16cea45d711cc Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 12:15:41 -0500 Subject: [PATCH 008/161] wip: package updates --- .../Logging/DebugWebsocketSink.cs | 42 +++++++++------- src/PepperDash.Core/PepperDash.Core.csproj | 20 ++++---- .../PepperDash.Essentials.Core.csproj | 3 +- ...epperDash.Essentials.Devices.Common.csproj | 3 +- ...Essentials.MobileControl.Messengers.csproj | 3 +- ...PepperDash.Essentials.MobileControl.csproj | 3 +- .../MobileControlWebsocketServer.cs | 48 ++++++++++--------- .../PepperDash.Essentials.csproj | 4 +- 8 files changed, 70 insertions(+), 56 deletions(-) diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 856abbdb..3372970a 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -28,7 +28,7 @@ namespace PepperDash.Core /// public class DebugWebsocketSink : ILogEventSink { - private HttpServer _httpsServer; + private WebSocketServer _wsServer; private string _path = "/debug/join/"; private const string _certificateName = "selfCres"; @@ -38,8 +38,8 @@ namespace PepperDash.Core { get { - if(_httpsServer == null) return 0; - return _httpsServer.Port; + if(_wsServer == null) return 0; + return _wsServer.Port; } } @@ -47,15 +47,15 @@ namespace PepperDash.Core { get { - if (_httpsServer == null) return ""; - return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}"; + if (_wsServer == null) return ""; + return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_wsServer.Port}{_wsServer.WebSocketServices[_path].Path}"; } } /// /// Gets or sets the IsRunning /// - public bool IsRunning { get => _httpsServer?.IsListening ?? false; } + public bool IsRunning { get => _wsServer?.IsListening ?? false; } private readonly ITextFormatter _textFormatter; @@ -118,12 +118,12 @@ namespace PepperDash.Core /// public void Emit(LogEvent logEvent) { - if (_httpsServer == null || !_httpsServer.IsListening) return; + if (_wsServer == null || !_wsServer.IsListening) return; var sw = new StringWriter(); _textFormatter.Format(logEvent, sw); - _httpsServer.WebSocketServices.Broadcast(sw.ToString()); + _wsServer.WebSocketServices.Broadcast(sw.ToString()); } @@ -142,17 +142,16 @@ namespace PepperDash.Core { try { - _httpsServer = new HttpServer(port, true); - + ServerSslConfiguration sslConfig = null; if (!string.IsNullOrWhiteSpace(certPath)) { Debug.Console(0, "Assigning SSL Configuration"); - _httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword)) + sslConfig = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword)) { ClientCertificateRequired = false, CheckCertificateRevocation = false, - EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, + EnabledSslProtocols = SslProtocols.Tls12, //this is just to test, you might want to actually validate ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { @@ -161,11 +160,18 @@ namespace PepperDash.Core } }; } + + _wsServer = new WebSocketServer(port, true); + if (sslConfig != null) + { + _wsServer.SslConfiguration.ServerCertificate = sslConfig.ServerCertificate; + } + Debug.Console(0, "Adding Debug Client Service"); - _httpsServer.AddWebSocketService(_path); + _wsServer.AddWebSocketService(_path); Debug.Console(0, "Assigning Log Info"); - _httpsServer.Log.Level = LogLevel.Trace; - _httpsServer.Log.Output = (d, s) => + _wsServer.Log.Level = LogLevel.Trace; + _wsServer.Log.Output = (d, s) => { uint level; @@ -198,7 +204,7 @@ namespace PepperDash.Core }; Debug.Console(0, "Starting"); - _httpsServer.Start(); + _wsServer.Start(); Debug.Console(0, "Ready"); } catch (Exception ex) @@ -213,9 +219,9 @@ namespace PepperDash.Core public void StopServer() { Debug.Console(0, "Stopping Websocket Server"); - _httpsServer?.Stop(); + _wsServer?.Stop(); - _httpsServer = null; + _wsServer = null; } } diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj index 875e6275..d35414f6 100644 --- a/src/PepperDash.Core/PepperDash.Core.csproj +++ b/src/PepperDash.Core/PepperDash.Core.csproj @@ -5,7 +5,7 @@ PepperDash.Core PepperDashCore - net472 + net8 true en bin\$(Configuration)\ @@ -42,18 +42,18 @@ - - + + global,NewtonsoftJson - - - - - - - + + + + + + + diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index a474d8c3..ee84b176 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -25,7 +25,8 @@ bin\$(Configuration)\PepperDash_Essentials_Core.xml - + + diff --git a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj index d1776f73..c5a38981 100644 --- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj +++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj @@ -29,6 +29,7 @@ - + + \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index 7eaefc16..0d948cc0 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -33,7 +33,8 @@ - + + diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index 73d648f7..2f8ad112 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -38,7 +38,8 @@ - + + diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 3b55120f..210dc577 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -1,29 +1,26 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Net.Http; +using System.Collections.Concurrent; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; 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; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using System.Collections.Generic; +using System; +using System.Net.Http; +using Crestron.SimplSharp; +using System.Linq; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core.Web; -using PepperDash.Essentials.RoomBridges; +using Crestron.SimplSharp.WebScripting; using PepperDash.Essentials.WebApiHandlers; -using Serilog.Events; using WebSocketSharp; using WebSocketSharp.Net; using WebSocketSharp.Server; - +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using System.IO; +using Newtonsoft.Json; +using PepperDash.Core; +using Serilog.Events; +using PepperDash.Essentials.RoomBridges; namespace PepperDash.Essentials.WebSocketServer { @@ -51,7 +48,7 @@ namespace PepperDash.Essentials.WebSocketServer private HttpServer _server; /// - /// Gets the HttpServer instance + /// Gets the WebSocketServer instance /// public HttpServer Server => _server; @@ -269,24 +266,31 @@ namespace PepperDash.Essentials.WebSocketServer _server = new HttpServer(Port, false); - _server.OnGet += Server_OnGet; - _server.OnOptions += Server_OnOptions; + + _server.OnGet += (sender, e) => Server_OnGet(sender, e); + + _server.OnOptions += (sender, e) => Server_OnOptions(sender, e); if (_parent.Config.DirectServer.Logging.EnableRemoteLogging) { - _server.OnPost += Server_OnPost; + _server.OnPost += (sender, e) => Server_OnPost(sender, e); } if (_parent.Config.DirectServer.Secure) { this.LogInformation("Adding SSL Configuration to server"); - _server.SslConfiguration = new ServerSslConfiguration(new X509Certificate2($"\\user\\{certificateName}.pfx", certificatePassword)) + + ServerSslConfiguration sslConfig = null; + + sslConfig = new ServerSslConfiguration(new X509Certificate2($"\\user\\{certificateName}.pfx", certificatePassword)) { ClientCertificateRequired = false, CheckCertificateRevocation = false, EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 }; + + _server.SslConfiguration.ServerCertificate = sslConfig.ServerCertificate; } _server.Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index c8a4dc7e..856b881f 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -51,8 +51,8 @@ - - + + From eafade9569592c5c667e2f31e081cac5d9b9db7c Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 12:33:32 -0500 Subject: [PATCH 009/161] docs: add XML comments --- src/PepperDash.Core/CoreInterfaces.cs | 8 +- .../Logging/DebugWebsocketSink.cs | 136 ++++++++++++------ 2 files changed, 100 insertions(+), 44 deletions(-) diff --git a/src/PepperDash.Core/CoreInterfaces.cs b/src/PepperDash.Core/CoreInterfaces.cs index c334cc00..7eb565dd 100644 --- a/src/PepperDash.Core/CoreInterfaces.cs +++ b/src/PepperDash.Core/CoreInterfaces.cs @@ -15,8 +15,8 @@ namespace PepperDash.Core /// public interface IKeyed { - /// - /// Unique Key + /// + /// Gets the unique key associated with the object. /// [JsonProperty("key")] string Key { get; } @@ -27,8 +27,8 @@ namespace PepperDash.Core /// public interface IKeyName : IKeyed { - /// - /// Isn't it obvious :) + /// + /// Gets the name associated with the current object. /// [JsonProperty("name")] string Name { get; } diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 3372970a..5f4f1de2 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -1,32 +1,36 @@ -extern alias NewtonsoftJson; +extern alias NewtonsoftJson; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Crestron.SimplSharp; +using Org.BouncyCastle.Asn1.X509; using Serilog; +using Serilog.Configuration; using Serilog.Core; using Serilog.Events; -using Serilog.Configuration; -using WebSocketSharp.Server; -using Crestron.SimplSharp; -using WebSocketSharp; -using System.Security.Authentication; -using WebSocketSharp.Net; -using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; -using System.IO; -using Org.BouncyCastle.Asn1.X509; using Serilog.Formatting; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using Serilog.Formatting.Json; +using System.IO; +using System.Security.Authentication; +using WebSocketSharp; +using WebSocketSharp.Server; +using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; +using WebSocketSharp.Net; namespace PepperDash.Core { /// - /// Represents a DebugWebsocketSink + /// Provides a WebSocket-based logging sink for debugging purposes, allowing log events to be broadcast to connected + /// WebSocket clients. /// - public class DebugWebsocketSink : ILogEventSink + /// This class implements the interface and is designed to send + /// formatted log events to WebSocket clients connected to a secure WebSocket server. The server is hosted locally + /// and uses a self-signed certificate for SSL/TLS encryption. + public class DebugWebsocketSink : ILogEventSink, IKeyed { private WebSocketServer _wsServer; @@ -34,6 +38,9 @@ namespace PepperDash.Core private const string _certificateName = "selfCres"; private const string _certificatePassword = "cres12345"; + /// + /// Gets the port number on which the HTTPS server is currently running. + /// public int Port { get { @@ -43,6 +50,11 @@ namespace PepperDash.Core } } + /// + /// Gets the WebSocket URL for the current server instance. + /// + /// The URL is dynamically constructed based on the server's current IP address, port, + /// and WebSocket path. public string Url { get @@ -53,20 +65,30 @@ namespace PepperDash.Core } /// - /// Gets or sets the IsRunning + /// Gets a value indicating whether the WebSocket server is currently listening for incoming connections. /// - public bool IsRunning { get => _wsServer?.IsListening ?? false; } - + public bool IsRunning { get => _wsServer?.IsListening ?? false; } + + /// + public string Key => "DebugWebsocketSink"; private readonly ITextFormatter _textFormatter; + /// + /// Initializes a new instance of the class with the specified text formatter. + /// + /// This constructor initializes the WebSocket sink and ensures that a certificate is + /// available for secure communication. If the required certificate does not exist, it will be created + /// automatically. Additionally, the sink is configured to stop the server when the program is + /// stopping. + /// The text formatter used to format log messages. If null, a default JSON formatter is used. public DebugWebsocketSink(ITextFormatter formatProvider) { _textFormatter = formatProvider ?? new JsonFormatter(); if (!File.Exists($"\\user\\{_certificateName}.pfx")) - CreateCert(null); + CreateCert(); CrestronEnvironment.ProgramStatusEventHandler += type => { @@ -77,7 +99,7 @@ namespace PepperDash.Core }; } - private void CreateCert(string[] args) + private static void CreateCert() { try { @@ -109,13 +131,17 @@ namespace PepperDash.Core catch (Exception ex) { //Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); - CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace)); + Debug.LogError("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); } } /// - /// Emit method + /// Sends a log event to all connected WebSocket clients. /// + /// The log event is formatted using the configured text formatter and then broadcasted + /// to all clients connected to the WebSocket server. If the WebSocket server is not initialized or not + /// listening, the method exits without performing any action. + /// The log event to be formatted and broadcasted. Cannot be null. public void Emit(LogEvent logEvent) { if (_wsServer == null || !_wsServer.IsListening) return; @@ -123,16 +149,19 @@ namespace PepperDash.Core var sw = new StringWriter(); _textFormatter.Format(logEvent, sw); - _wsServer.WebSocketServices.Broadcast(sw.ToString()); - + _wsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString()); } /// - /// StartServerAndSetPort method + /// Starts the WebSocket server on the specified port and configures it with the appropriate certificate. /// + /// This method initializes the WebSocket server and binds it to the specified port. It + /// also applies the server's certificate for secure communication. Ensure that the port is not already in use + /// and that the certificate file is accessible. + /// The port number on which the WebSocket server will listen. Must be a valid, non-negative port number. public void StartServerAndSetPort(int port) { - Debug.Console(0, "Starting Websocket Server on port: {0}", port); + Debug.LogInformation("Starting Websocket Server on port: {0}", port); Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword); @@ -146,7 +175,7 @@ namespace PepperDash.Core if (!string.IsNullOrWhiteSpace(certPath)) { - Debug.Console(0, "Assigning SSL Configuration"); + Debug.LogInformation("Assigning SSL Configuration"); sslConfig = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword)) { ClientCertificateRequired = false, @@ -155,7 +184,7 @@ namespace PepperDash.Core //this is just to test, you might want to actually validate ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { - Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered"); + Debug.LogInformation("HTTPS ClientCerticateValidation Callback triggered"); return true; } }; @@ -167,9 +196,9 @@ namespace PepperDash.Core _wsServer.SslConfiguration.ServerCertificate = sslConfig.ServerCertificate; } - Debug.Console(0, "Adding Debug Client Service"); + Debug.LogInformation("Adding Debug Client Service"); _wsServer.AddWebSocketService(_path); - Debug.Console(0, "Assigning Log Info"); + Debug.LogInformation("Assigning Log Info"); _wsServer.Log.Level = LogLevel.Trace; _wsServer.Log.Output = (d, s) => { @@ -200,36 +229,48 @@ namespace PepperDash.Core break; } - Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); + Debug.LogInformation("{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); }; - Debug.Console(0, "Starting"); + Debug.LogInformation("Starting"); _wsServer.Start(); - Debug.Console(0, "Ready"); + Debug.LogInformation("Ready"); } catch (Exception ex) { - Debug.Console(0, "WebSocket Failed to start {0}", ex.Message); + Debug.LogError("WebSocket Failed to start {0}", ex.Message); } } /// - /// StopServer method + /// Stops the WebSocket server if it is currently running. /// + /// This method halts the WebSocket server and releases any associated resources. After + /// calling this method, the server will no longer accept or process incoming connections. public void StopServer() { - Debug.Console(0, "Stopping Websocket Server"); + Debug.LogInformation("Stopping Websocket Server"); _wsServer?.Stop(); _wsServer = null; } } + /// + /// Configures the logger to write log events to a debug WebSocket sink. + /// + /// This extension method allows you to direct log events to a WebSocket sink for debugging + /// purposes. public static class DebugWebsocketSinkExtensions { /// - /// DebugWebsocketSink method + /// Configures a logger to write log events to a debug WebSocket sink. /// + /// This method adds a sink that writes log events to a WebSocket for debugging purposes. + /// It is typically used during development to stream log events in real-time. + /// The logger sink configuration to apply the WebSocket sink to. + /// An optional text formatter to format the log events. If not provided, a default formatter will be used. + /// A object that can be used to further configure the logger. public static LoggerConfiguration DebugWebsocketSink( this LoggerSinkConfiguration loggerConfiguration, ITextFormatter formatProvider = null) @@ -239,12 +280,19 @@ namespace PepperDash.Core } /// - /// Represents a DebugClient + /// Represents a WebSocket client for debugging purposes, providing connection lifecycle management and message + /// handling functionality. /// + /// The class extends to handle + /// WebSocket connections, including events for opening, closing, receiving messages, and errors. It tracks the + /// duration of the connection and logs relevant events for debugging. public class DebugClient : WebSocketBehavior { private DateTime _connectionTime; + /// + /// Gets the duration of time the WebSocket connection has been active. + /// public TimeSpan ConnectedDuration { get @@ -260,41 +308,49 @@ namespace PepperDash.Core } } + /// + /// Initializes a new instance of the class. + /// + /// This constructor creates a new instance and logs its creation. public DebugClient() { - Debug.Console(0, "DebugClient Created"); + Debug.LogInformation("DebugClient Created"); } + /// protected override void OnOpen() { base.OnOpen(); var url = Context.WebSocket.Url; - Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); + Debug.LogInformation("New WebSocket Connection from: {0}", url); _connectionTime = DateTime.Now; } + /// protected override void OnMessage(MessageEventArgs e) { base.OnMessage(e); - Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data); + Debug.LogInformation("WebSocket UiClient Message: {0}", e.Data); } + /// protected override void OnClose(CloseEventArgs e) { base.OnClose(e); - Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); + Debug.LogInformation("WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); } + /// protected override void OnError(WebSocketSharp.ErrorEventArgs e) { base.OnError(e); - Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); + Debug.LogError("WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); } } } From 9febbf2557121cd8e8627015601400f0659d70cc Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 12:46:08 -0500 Subject: [PATCH 010/161] chore: fix issues related to remving crestron usings --- .../JsonToSimpl/JsonToSimplMaster.cs | 9 +-- .../Logging/DebugWebsocketSink.cs | 28 +++------ .../Plugins/PluginLoader.cs | 58 +++++++++---------- 3 files changed, 39 insertions(+), 56 deletions(-) diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs index bc965e92..fcf6e0f8 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs @@ -167,11 +167,7 @@ namespace PepperDash.Core.JsonToSimpl /// public static JObject ParseObject(string json) { - #if NET6_0 using (var reader = new JsonTextReader(new System.IO.StringReader(json))) -#else - using (var reader = new JsonTextReader(new System.IO.StringReader(json))) -#endif { var startDepth = reader.Depth; var obj = JObject.Load(reader); @@ -191,11 +187,8 @@ namespace PepperDash.Core.JsonToSimpl /// public static JArray ParseArray(string json) { - #if NET6_0 + using (var reader = new JsonTextReader(new System.IO.StringReader(json))) -#else - using (var reader = new JsonTextReader(new System.IO.StringReader(json))) -#endif { var startDepth = reader.Depth; var obj = JArray.Load(reader); diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 5f4f1de2..81d981ec 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -1,4 +1,4 @@ -extern alias NewtonsoftJson; +extern alias NewtonsoftJson; using System; using System.Collections.Generic; @@ -34,7 +34,7 @@ namespace PepperDash.Core { private WebSocketServer _wsServer; - private string _path = "/debug/join/"; + private readonly string _path = "/debug/join/"; private const string _certificateName = "selfCres"; private const string _certificatePassword = "cres12345"; @@ -102,31 +102,21 @@ namespace PepperDash.Core private static void CreateCert() { try - { - //Debug.Console(0,"CreateCert Creating Utility"); - CrestronConsole.PrintLine("CreateCert Creating Utility"); - //var utility = new CertificateUtility(); + { var utility = new BouncyCertificate(); - //Debug.Console(0, "CreateCert Calling CreateCert"); - CrestronConsole.PrintLine("CreateCert Calling CreateCert"); - //utility.CreateCert(); + var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0); - //Debug.Console(0, "DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress); CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress)); - var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), new[] { string.Format("{0}.{1}", hostName, domainName), ipAddress }, new[] { KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth }); - //Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested - //Debug.Print($"CreateCert Storing Certificate To My.LocalMachine"); - //utility.AddCertToStore(certificate, StoreName.My, StoreLocation.LocalMachine); - //Debug.Console(0, "CreateCert Saving Cert to \\user\\"); - CrestronConsole.PrintLine("CreateCert Saving Cert to \\user\\"); + var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), [string.Format("{0}.{1}", hostName, domainName), ipAddress], [KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth]); + + //Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested + utility.CertificatePassword = _certificatePassword; - utility.WriteCertificate(certificate, @"\user\", _certificateName); - //Debug.Console(0, "CreateCert Ending CreateCert"); - CrestronConsole.PrintLine("CreateCert Ending CreateCert"); + utility.WriteCertificate(certificate, @"\user\", _certificateName); } catch (Exception ex) { diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index 17619b2f..0f76b528 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Reflection; using Crestron.SimplSharp; +using System.Reflection; +using System.IO; using Newtonsoft.Json; using System.Reflection.PortableExecutable; using System.Reflection.Metadata; @@ -95,7 +95,7 @@ namespace PepperDash.Essentials { Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials"); // Get the loaded assembly filenames - var appDi = new SystemIO.DirectoryInfo(Global.ApplicationDirectoryPathPrefix); + var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); var assemblyFiles = appDi.GetFiles("*.dll"); Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length); @@ -172,8 +172,8 @@ namespace PepperDash.Essentials { List referencedAssemblies = new List(); - using (SystemIO.FileStream fs = new SystemIO.FileStream(filePath, SystemIO.FileMode.Open, - SystemIO.FileAccess.Read, SystemIO.FileShare.ReadWrite)) + using (FileStream fs = new FileStream(filePath, FileMode.Open, + FileAccess.Read, FileShare.ReadWrite)) using (PEReader peReader = new PEReader(fs)) { if (!peReader.HasMetadata) @@ -252,7 +252,7 @@ namespace PepperDash.Essentials var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath); if (!isCompatible) { - string fileName = CrestronIO.Path.GetFileName(filePath); + string fileName = Path.GetFileName(filePath); Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy); @@ -275,10 +275,10 @@ namespace PepperDash.Essentials } return null; } - catch(SystemIO.FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) + catch(FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) { // Get the assembly name from the file path - string assemblyName = CrestronIO.Path.GetFileNameWithoutExtension(filePath); + string assemblyName = Path.GetFileNameWithoutExtension(filePath); // Try to find the already loaded assembly var existingAssembly = AppDomain.CurrentDomain.GetAssemblies() @@ -298,7 +298,7 @@ namespace PepperDash.Essentials } catch(Exception ex) { - string fileName = CrestronIO.Path.GetFileName(filePath); + string fileName = Path.GetFileName(filePath); // Check if this might be a .NET Framework compatibility issue if (ex.Message.Contains("Could not load type") || @@ -415,14 +415,14 @@ namespace PepperDash.Essentials { Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder..."); - var pluginDi = new SystemIO.DirectoryInfo(_pluginDirectory); + var pluginDi = new DirectoryInfo(_pluginDirectory); var pluginFiles = pluginDi.GetFiles("*.dll"); if (pluginFiles.Length > 0) { - if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath)) + if (!Directory.Exists(_loadedPluginsDirectoryPath)) { - SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath); + Directory.CreateDirectory(_loadedPluginsDirectoryPath); } } @@ -439,14 +439,14 @@ namespace PepperDash.Essentials filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name; // Check if there is a previous file in the loadedPlugins directory and delete - if (SystemIO.File.Exists(filePath)) + if (File.Exists(filePath)) { Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath); - SystemIO.File.Delete(filePath); + File.Delete(filePath); } // Move the file - SystemIO.File.Move(pluginFile.FullName, filePath); + File.Move(pluginFile.FullName, filePath); Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath); } else @@ -470,7 +470,7 @@ namespace PepperDash.Essentials static void UnzipAndMoveCplzArchives() { Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder..."); - var di = new SystemIO.DirectoryInfo(_pluginDirectory); + var di = new DirectoryInfo(_pluginDirectory); var zFiles = di.GetFiles("*.cplz"); //// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users @@ -485,16 +485,16 @@ namespace PepperDash.Essentials if (cplzFiles.Length > 0) { - if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath)) + if (!Directory.Exists(_loadedPluginsDirectoryPath)) { - SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath); + Directory.CreateDirectory(_loadedPluginsDirectoryPath); } } foreach (var zfi in cplzFiles) { - SystemIO.Directory.CreateDirectory(_tempDirectory); - var tempDi = new SystemIO.DirectoryInfo(_tempDirectory); + Directory.CreateDirectory(_tempDirectory); + var tempDi = new DirectoryInfo(_tempDirectory); Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName); var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName); @@ -512,14 +512,14 @@ namespace PepperDash.Essentials filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name; // Check if there is a previous file in the loadedPlugins directory and delete - if (SystemIO.File.Exists(filePath)) + if (File.Exists(filePath)) { Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath); - SystemIO.File.Delete(filePath); + File.Delete(filePath); } // Move the file - SystemIO.File.Move(tempFile.FullName, filePath); + File.Move(tempFile.FullName, filePath); Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath); } else @@ -535,7 +535,7 @@ namespace PepperDash.Essentials } // Delete the .cplz and the temp directory - SystemIO.Directory.Delete(_tempDirectory, true); + Directory.Delete(_tempDirectory, true); zfi.Delete(); } @@ -548,7 +548,7 @@ namespace PepperDash.Essentials static void LoadPluginAssemblies() { Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder..."); - var pluginDi = new CrestronIO.DirectoryInfo(_loadedPluginsDirectoryPath); + var pluginDi = new DirectoryInfo(_loadedPluginsDirectoryPath); var pluginFiles = pluginDi.GetFiles("*.dll"); Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length); @@ -618,7 +618,7 @@ namespace PepperDash.Essentials // Check if any of the loader exceptions are due to missing assemblies foreach (var loaderEx in e.LoaderExceptions) { - if (loaderEx is SystemIO.FileNotFoundException fileNotFoundEx) + if (loaderEx is FileNotFoundException fileNotFoundEx) { string missingAssembly = fileNotFoundEx.FileName; if (!string.IsNullOrEmpty(missingAssembly)) @@ -628,7 +628,7 @@ namespace PepperDash.Essentials // Add to incompatible plugins with dependency information IncompatiblePlugins.Add(new IncompatiblePlugin( - CrestronIO.Path.GetFileName(missingAssembly), + Path.GetFileName(missingAssembly), $"Missing dependency required by {loadedAssembly.Name}", loadedAssembly.Name)); } @@ -797,7 +797,7 @@ namespace PepperDash.Essentials { Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory); - if (SystemIO.Directory.Exists(_pluginDirectory)) + if (Directory.Exists(_pluginDirectory)) { Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins"); @@ -807,7 +807,7 @@ namespace PepperDash.Essentials // Deal with any .cplz files UnzipAndMoveCplzArchives(); - if (SystemIO.Directory.Exists(_loadedPluginsDirectoryPath)) + if (Directory.Exists(_loadedPluginsDirectoryPath)) { // Load the assemblies from the loadedPlugins folder into the AppDomain LoadPluginAssemblies(); From 04c455752869660db1840500d98b7281a7509833 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 13:07:21 -0500 Subject: [PATCH 011/161] docs: update XML docs --- .../Plugins/IPluginDeviceFactory.cs | 8 +- .../Plugins/IncompatiblePlugin.cs | 47 + .../Plugins/LoadedAssembly.cs | 46 + .../Plugins/Net8CompatibleAttribute.cs | 20 + .../Plugins/PluginLoader.cs | 1537 ++++++++--------- 5 files changed, 874 insertions(+), 784 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Plugins/IncompatiblePlugin.cs create mode 100644 src/PepperDash.Essentials.Core/Plugins/LoadedAssembly.cs create mode 100644 src/PepperDash.Essentials.Core/Plugins/Net8CompatibleAttribute.cs diff --git a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs index caad4b57..72b6ad7b 100644 --- a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs @@ -17,14 +17,14 @@ namespace PepperDash.Essentials.Core } /// - /// Defines a class that is capable of loading custom plugin device types for development purposes + /// Defines a factory for creating plugin development devices, including support for specific framework versions. /// - [Obsolete("This interface is obsolete and will be removed in a future version." + - " Use IPluginDeviceFactory instead and check Global.IsRunningDevelopmentVersion to determine if the Essentials framework is in development mode.")] + /// This interface extends to provide additional functionality + /// specific to plugin development environments. public interface IPluginDevelopmentDeviceFactory : IPluginDeviceFactory { /// - /// Gets a list of all the development versions of the Essentials framework that are supported by this factory. + /// Gets a list of Essentials versions that this device is compatible with. /// List DevelopmentEssentialsFrameworkVersions { get; } } diff --git a/src/PepperDash.Essentials.Core/Plugins/IncompatiblePlugin.cs b/src/PepperDash.Essentials.Core/Plugins/IncompatiblePlugin.cs new file mode 100644 index 00000000..8cce6f62 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Plugins/IncompatiblePlugin.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; + + +namespace PepperDash.Essentials; + +/// +/// Represents a plugin that is incompatible with the current system or configuration. +/// +/// This class provides details about an incompatible plugin, including its name, the reason for the +/// incompatibility, and the plugin that triggered the incompatibility. The triggering plugin can be updated dynamically +/// using the method. +/// +/// +/// +public class IncompatiblePlugin(string name, string reason, string triggeredBy = null) +{ + /// + /// Gets the name associated with the object. + /// + [JsonProperty("name")] + public string Name { get; private set; } = name; + + /// + /// Gets the reason associated with the current operation or response. + /// + [JsonProperty("reason")] + public string Reason { get; private set; } = reason; + + /// + /// Gets the identifier of the entity or process that triggered the current operation. + /// + [JsonProperty("triggeredBy")] + public string TriggeredBy { get; private set; } = triggeredBy ?? "Direct load"; + + /// + /// Updates the name of the plugin that triggered the current operation. + /// + /// The name of the triggering plugin. Must not be null or empty. If the value is null or empty, the operation is + /// ignored. + public void UpdateTriggeringPlugin(string triggeringPlugin) + { + if (!string.IsNullOrEmpty(triggeringPlugin)) + { + TriggeredBy = triggeringPlugin; + } + } +} diff --git a/src/PepperDash.Essentials.Core/Plugins/LoadedAssembly.cs b/src/PepperDash.Essentials.Core/Plugins/LoadedAssembly.cs new file mode 100644 index 00000000..64c639e5 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Plugins/LoadedAssembly.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using Newtonsoft.Json; + + +namespace PepperDash.Essentials; + +/// +/// Represents an assembly that has been loaded, including its name, version, and the associated instance. +/// +/// This class provides information about a loaded assembly, including its name and version as strings, +/// and the associated object. The assembly instance can be updated using the +/// method. +/// +/// +/// +public class LoadedAssembly(string name, string version, Assembly assembly) +{ + /// + /// Gets the name associated with the object. + /// + [JsonProperty("name")] + public string Name { get; private set; } = name; + + /// + /// Gets the version of the object as a string. + /// + [JsonProperty("version")] + public string Version { get; private set; } = version; + + /// + /// Gets the assembly associated with the current instance. + /// + [JsonIgnore] + public Assembly Assembly { get; private set; } = assembly; + + /// + /// Sets the assembly associated with the current instance. + /// + /// The to associate with the current instance. Cannot be . + public void SetAssembly(Assembly assembly) + { + Assembly = assembly; + } +} diff --git a/src/PepperDash.Essentials.Core/Plugins/Net8CompatibleAttribute.cs b/src/PepperDash.Essentials.Core/Plugins/Net8CompatibleAttribute.cs new file mode 100644 index 00000000..1a5b250a --- /dev/null +++ b/src/PepperDash.Essentials.Core/Plugins/Net8CompatibleAttribute.cs @@ -0,0 +1,20 @@ +using System; + + +namespace PepperDash.Essentials; + +/// +/// Indicates whether the assembly is compatible with .NET 8. +/// +/// This attribute is used to specify compatibility with .NET 8 for an assembly. By default, the +/// assembly is considered compatible unless explicitly marked otherwise. +/// A boolean value indicating whether the assembly is compatible with .NET 8. The default value is . +[AttributeUsage(AttributeTargets.Assembly)] +public class Net8CompatibleAttribute(bool isCompatible = true) : Attribute +{ + /// + /// Gets a value indicating whether the current object is compatible with the required conditions. + /// + public bool IsCompatible { get; } = isCompatible; +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index 0f76b528..6a857682 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -14,429 +14,569 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +/// +/// Provides functionality for loading and managing plugins and assemblies in the application. +/// +/// The class is responsible for discovering, loading, and managing assemblies +/// and plugins, including handling compatibility checks for .NET 8. It supports loading assemblies from the program +/// directory, plugins folder, and .cplz archives. Additionally, it tracks incompatible plugins and provides reporting +/// capabilities for loaded assemblies and their versions. +public static class PluginLoader { /// - /// Deals with loading plugins at runtime + /// Gets the list of assemblies that have been loaded into the application. /// - public static class PluginLoader - { - /// - /// The complete list of loaded assemblies. Includes Essentials Framework assemblies and plugins - /// - public static List LoadedAssemblies { get; private set; } + public static List LoadedAssemblies { get; private set; } - /// - /// The list of assemblies loaded from the plugins folder - /// - static List LoadedPluginFolderAssemblies; + /// + /// Represents a collection of assemblies loaded from the plugin folder. + /// + /// This field is used to store assemblies that have been dynamically loaded from a designated + /// plugin folder. It is intended for internal use and should not be modified directly. + private static readonly List LoadedPluginFolderAssemblies; - /// - /// List of plugins that were found to be incompatible with .NET 8 - /// - public static List IncompatiblePlugins { get; private set; } + /// + /// Gets the list of plugins that are incompatible with the current system or configuration. + /// + /// This property provides information about plugins that are not supported or cannot function + /// correctly in the current environment. Use this list to identify and handle incompatible plugins appropriately in + /// your application logic. + public static List IncompatiblePlugins { get; private set; } - /// - /// The assembly for the Essentials Framework - /// - public static LoadedAssembly EssentialsAssembly { get; private set; } + /// + /// Gets the loaded assembly that contains the core functionality of the application. + /// + public static LoadedAssembly EssentialsAssembly { get; private set; } - /// - /// The assembly for the PepperDash Core - /// - public static LoadedAssembly PepperDashCoreAssembly { get; private set; } + /// + /// Gets the loaded assembly information for the PepperDash Core library. + /// + public static LoadedAssembly PepperDashCoreAssembly { get; private set; } - /// - /// The list of assemblies loaded from the Essentials plugins folder - /// - public static List EssentialsPluginAssemblies { get; private set; } + /// + /// Gets the list of assemblies that are Essentials plugins loaded by the application. + /// + public static List EssentialsPluginAssemblies { get; private set; } - /// - /// The directory to look in for .cplz plugin packages - /// - static string _pluginDirectory => Global.FilePathPrefix + "plugins"; + /// + /// Gets the directory path where plugins are stored. + /// + private static string PluginDirectory => Global.FilePathPrefix + "plugins"; - /// - /// The directory where plugins will be moved to and loaded from - /// - static string _loadedPluginsDirectoryPath => _pluginDirectory + Global.DirectorySeparator + "loadedAssemblies"; + /// + /// Gets the directory path where loaded plugin assemblies are stored. + /// + private static string LoadedPluginsDirectoryPath => PluginDirectory + Global.DirectorySeparator + "loadedAssemblies"; - // The temp directory where .cplz archives will be unzipped to - static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp"; + /// + /// Gets the path to the temporary directory used by the plugin. + /// + private static string TempDirectory => PluginDirectory + Global.DirectorySeparator + "temp"; - // Known incompatible types in .NET 8 - private static readonly HashSet KnownIncompatibleTypes = new HashSet - { + /// + /// The directory to look in for .cplz plugin packages + /// + static string _pluginDirectory => Global.FilePathPrefix + "plugins"; + + /// + /// The directory where plugins will be moved to and loaded from + /// + static string _loadedPluginsDirectoryPath => _pluginDirectory + Global.DirectorySeparator + "loadedAssemblies"; + + // The temp directory where .cplz archives will be unzipped to + static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp"; + + /// + /// Represents a collection of fully qualified type names that are known to be incompatible with the current + /// application or framework. + /// + /// This collection contains the names of types that are deprecated, obsolete, or otherwise + /// incompatible with the intended usage of the application. These types may represent security risks, unsupported + /// features, or legacy APIs that should be avoided. + private static readonly HashSet KnownIncompatibleTypes = + [ "System.Net.ICertificatePolicy", - "System.Security.Cryptography.SHA1CryptoServiceProvider", - "System.Web.HttpUtility", - "System.Configuration.ConfigurationManager", - "System.Web.Services.Protocols.SoapHttpClientProtocol", - "System.Runtime.Remoting", - "System.EnterpriseServices", - "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter", - "System.Security.SecurityManager", - "System.Security.Permissions.FileIOPermission", - "System.AppDomain.CreateDomain" - }; + "System.Security.Cryptography.SHA1CryptoServiceProvider", + "System.Web.HttpUtility", + "System.Configuration.ConfigurationManager", + "System.Web.Services.Protocols.SoapHttpClientProtocol", + "System.Runtime.Remoting", + "System.EnterpriseServices", + "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter", + "System.Security.SecurityManager", + "System.Security.Permissions.FileIOPermission", + "System.AppDomain.CreateDomain" + ]; - static PluginLoader() + /// + /// Initializes static members of the class. + /// + /// This static constructor initializes the collections used to manage plugin assemblies and + /// track incompatible plugins. + static PluginLoader() + { + LoadedAssemblies = []; + LoadedPluginFolderAssemblies = []; + EssentialsPluginAssemblies = []; + IncompatiblePlugins = []; + } + + /// + /// Loads and registers assemblies from the application's directory that match specific naming patterns. + /// + /// This method scans the application's directory for assemblies with filenames containing + /// "Essentials" or "PepperDash" and registers them in the collection. It also + /// assigns specific assemblies to predefined properties, such as and , based on their names. Debug messages are logged at various stages to provide + /// detailed information about the process, including the number of assemblies found and their versions. This + /// method is intended to be used during application initialization to ensure required assemblies are loaded and + /// tracked. + public static void AddProgramAssemblies() + { + Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials"); + // Get the loaded assembly filenames + var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); + var assemblyFiles = appDi.GetFiles("*.dll"); + + Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length); + + + foreach (var fi in assemblyFiles.Where(fi => fi.Name.Contains("Essentials") || fi.Name.Contains("PepperDash"))) { - LoadedAssemblies = new List(); - LoadedPluginFolderAssemblies = new List(); - EssentialsPluginAssemblies = new List(); - IncompatiblePlugins = new List(); + string version = string.Empty; + Assembly assembly = null; + + switch (fi.Name) + { + case ("PepperDashEssentials.dll"): + { + version = Global.AssemblyVersion; + EssentialsAssembly = new LoadedAssembly(fi.Name, version, assembly); + break; + } + case ("PepperDash_Essentials_Core.dll"): + { + version = Global.AssemblyVersion; + break; + } + case ("Essentials Devices Common.dll"): + { + version = Global.AssemblyVersion; + break; + } + case ("PepperDashCore.dll"): + { + Debug.LogMessage(LogEventLevel.Verbose, "Found PepperDash_Core.dll"); + version = Debug.PepperDashCoreVersion; + Debug.LogMessage(LogEventLevel.Verbose, "PepperDash_Core Version: {0}", version); + PepperDashCoreAssembly = new LoadedAssembly(fi.Name, version, assembly); + break; + } + } + + LoadedAssemblies.Add(new LoadedAssembly(fi.Name, version, assembly)); } - /// - /// AddProgramAssemblies method - /// - public static void AddProgramAssemblies() + if (Debug.Level > 1) { - Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials"); - // Get the loaded assembly filenames - var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); - var assemblyFiles = appDi.GetFiles("*.dll"); + Debug.LogMessage(LogEventLevel.Verbose, "Loaded Assemblies:"); - Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length); - - foreach (var fi in assemblyFiles.Where(fi => fi.Name.Contains("Essentials") || fi.Name.Contains("PepperDash"))) + foreach (var assembly in LoadedAssemblies) { - string version = string.Empty; - Assembly assembly = null; - - switch (fi.Name) - { - case ("PepperDashEssentials.dll"): - { - version = Global.AssemblyVersion; - EssentialsAssembly = new LoadedAssembly(fi.Name, version, assembly); - break; - } - case ("PepperDash_Essentials_Core.dll"): - { - version = Global.AssemblyVersion; - break; - } - case ("Essentials Devices Common.dll"): - { - version = Global.AssemblyVersion; - break; - } - case ("PepperDashCore.dll"): - { - Debug.LogMessage(LogEventLevel.Verbose, "Found PepperDash_Core.dll"); - version = Debug.PepperDashCoreVersion; - Debug.LogMessage(LogEventLevel.Verbose, "PepperDash_Core Version: {0}", version); - PepperDashCoreAssembly = new LoadedAssembly(fi.Name, version, assembly); - break; - } - } - - LoadedAssemblies.Add(new LoadedAssembly(fi.Name, version, assembly)); - } - - if (Debug.Level > 1) - { - Debug.LogMessage(LogEventLevel.Verbose, "Loaded Assemblies:"); - - foreach (var assembly in LoadedAssemblies) - { - Debug.LogMessage(LogEventLevel.Verbose, "Assembly: {0}", assembly.Name); - } + Debug.LogMessage(LogEventLevel.Verbose, "Assembly: {0}", assembly.Name); } } + } - /// - /// SetEssentialsAssembly method - /// - public static void SetEssentialsAssembly(string name, Assembly assembly) + /// + /// Associates the specified assembly with the given name in the loaded assemblies collection. + /// + /// If an assembly with the specified name already exists in the loaded assemblies collection, + /// this method updates its associated assembly. If no matching name is found, the method does nothing. + /// The name used to identify the assembly. This value is case-sensitive and must not be null or empty. + /// The assembly to associate with the specified name. This value must not be null. + public static void SetEssentialsAssembly(string name, Assembly assembly) + { + var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name)); + + loadedAssembly?.SetAssembly(assembly); + } + + /// + /// Determines whether a plugin assembly is compatible with .NET 8. + /// + /// This method analyzes the provided assembly to determine compatibility with .NET 8 by checking + /// for known incompatible types, inspecting custom attributes, and collecting assembly references. If the analysis + /// encounters an error, the method returns with an appropriate error message. + /// The file path to the plugin assembly to analyze. + /// A tuple containing the following: if the plugin + /// is compatible with .NET 8; otherwise, . A + /// string providing the reason for incompatibility, or if the plugin is + /// compatible. A list of assembly references found in the + /// plugin. + public static (bool IsCompatible, string Reason, List References) IsPluginCompatibleWithNet8(string filePath) + { + try { - var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name)); + List referencedAssemblies = []; - if (loadedAssembly != null) + using FileStream fs = new(filePath, FileMode.Open, + FileAccess.Read, FileShare.ReadWrite); + using PEReader peReader = new(fs); + + if (!peReader.HasMetadata) + return (false, "Not a valid .NET assembly", referencedAssemblies); + + MetadataReader metadataReader = peReader.GetMetadataReader(); + + // Collect assembly references + foreach (var assemblyRefHandle in metadataReader.AssemblyReferences) { - loadedAssembly.SetAssembly(assembly); + var assemblyRef = metadataReader.GetAssemblyReference(assemblyRefHandle); + string assemblyName = metadataReader.GetString(assemblyRef.Name); + referencedAssemblies.Add(assemblyName); } + + // Check for references to known incompatible types + foreach (var typeRefHandle in metadataReader.TypeReferences) + { + var typeRef = metadataReader.GetTypeReference(typeRefHandle); + string typeNamespace = metadataReader.GetString(typeRef.Namespace); + string typeName = metadataReader.GetString(typeRef.Name); + + string fullTypeName = $"{typeNamespace}.{typeName}"; + if (KnownIncompatibleTypes.Contains(fullTypeName)) + { + return (false, $"Uses incompatible type: {fullTypeName}", referencedAssemblies); + } + } + + // Check for explicit .NET 8 compatibility attribute + bool hasNet8Attribute = false; + foreach (var customAttributeHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes()) + { + var customAttribute = metadataReader.GetCustomAttribute(customAttributeHandle); + var ctorHandle = customAttribute.Constructor; + + if (ctorHandle.Kind == HandleKind.MemberReference) + { + var memberRef = metadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle); + var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)memberRef.Parent); + + string typeName = metadataReader.GetString(typeRef.Name); + if (typeName == "Net8CompatibleAttribute" || typeName == "TargetFrameworkAttribute") + { + hasNet8Attribute = true; + break; + } + } + } + + if (hasNet8Attribute) + { + return (true, null, referencedAssemblies); + } + + // If we can't determine incompatibility, assume it's compatible + return (true, null, referencedAssemblies); } - - /// - /// Checks if a plugin is compatible with .NET 8 by examining its metadata - /// - /// Path to the plugin assembly - /// Tuple with compatibility result, reason if incompatible, and referenced assemblies - public static (bool IsCompatible, string Reason, List References) IsPluginCompatibleWithNet8(string filePath) + catch (Exception ex) { - try - { - List referencedAssemblies = new List(); - - using (FileStream fs = new FileStream(filePath, FileMode.Open, - FileAccess.Read, FileShare.ReadWrite)) - using (PEReader peReader = new PEReader(fs)) - { - if (!peReader.HasMetadata) - return (false, "Not a valid .NET assembly", referencedAssemblies); - - MetadataReader metadataReader = peReader.GetMetadataReader(); - - // Collect assembly references - foreach (var assemblyRefHandle in metadataReader.AssemblyReferences) - { - var assemblyRef = metadataReader.GetAssemblyReference(assemblyRefHandle); - string assemblyName = metadataReader.GetString(assemblyRef.Name); - referencedAssemblies.Add(assemblyName); - } - - // Check for references to known incompatible types - foreach (var typeRefHandle in metadataReader.TypeReferences) - { - var typeRef = metadataReader.GetTypeReference(typeRefHandle); - string typeNamespace = metadataReader.GetString(typeRef.Namespace); - string typeName = metadataReader.GetString(typeRef.Name); - - string fullTypeName = $"{typeNamespace}.{typeName}"; - if (KnownIncompatibleTypes.Contains(fullTypeName)) - { - return (false, $"Uses incompatible type: {fullTypeName}", referencedAssemblies); - } - } - - // Check for explicit .NET 8 compatibility attribute - bool hasNet8Attribute = false; - foreach (var customAttributeHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes()) - { - var customAttribute = metadataReader.GetCustomAttribute(customAttributeHandle); - var ctorHandle = customAttribute.Constructor; - - if (ctorHandle.Kind == HandleKind.MemberReference) - { - var memberRef = metadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle); - var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)memberRef.Parent); - - string typeName = metadataReader.GetString(typeRef.Name); - if (typeName == "Net8CompatibleAttribute" || typeName == "TargetFrameworkAttribute") - { - hasNet8Attribute = true; - break; - } - } - } - - if (hasNet8Attribute) - { - return (true, null, referencedAssemblies); - } - - // If we can't determine incompatibility, assume it's compatible - return (true, null, referencedAssemblies); - } - } - catch (Exception ex) - { - return (false, $"Error analyzing assembly: {ex.Message}", new List()); - } + return (false, $"Error analyzing assembly: {ex.Message}", new List()); } + } - /// - /// Loads an assembly via Reflection and adds it to the list of loaded assemblies - /// - /// Path to the assembly file - /// Name of the plugin requesting this assembly (null for direct loads) - static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null) + /// + /// Loads an assembly from the specified file path and verifies its compatibility with .NET 8. + /// + /// This method performs a compatibility check to ensure the assembly is compatible with .NET 8 + /// before attempting to load it. If the assembly is incompatible, it is added to the list of incompatible plugins, + /// and a warning is logged. If the assembly is already loaded, the existing instance is returned instead of + /// reloading it. Exceptions during the load process are handled internally, and appropriate log messages are + /// generated for issues such as: Incompatibility with .NET + /// 8. File load conflicts (e.g., an assembly with the same name is already + /// loaded). General errors, including potential .NET Framework + /// compatibility issues. + /// The full path to the assembly file to load. This cannot be null or empty. + /// An optional identifier for the entity requesting the load operation. This can be null. + /// A object representing the loaded assembly if the operation succeeds; otherwise, + /// . + private static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null) + { + try { - try - { - // Check .NET 8 compatibility before loading - var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath); - if (!isCompatible) - { - string fileName = Path.GetFileName(filePath); - Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); - - var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy); - IncompatiblePlugins.Add(incompatiblePlugin); - return null; - } - - var assembly = Assembly.LoadFrom(filePath); - if (assembly != null) - { - var assyVersion = GetAssemblyVersion(assembly); - var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly); - LoadedAssemblies.Add(loadedAssembly); - Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version); - return loadedAssembly; - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath); - } - return null; - } - catch(FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) - { - // Get the assembly name from the file path - string assemblyName = Path.GetFileNameWithoutExtension(filePath); - - // Try to find the already loaded assembly - var existingAssembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.OrdinalIgnoreCase)); - - if (existingAssembly != null) - { - Debug.LogMessage(LogEventLevel.Information, "Assembly '{0}' is already loaded, using existing instance", assemblyName); - var assyVersion = GetAssemblyVersion(existingAssembly); - var loadedAssembly = new LoadedAssembly(existingAssembly.GetName().Name, assyVersion, existingAssembly); - LoadedAssemblies.Add(loadedAssembly); - return loadedAssembly; - } - - Debug.LogMessage(LogEventLevel.Warning, "Assembly with same name already loaded but couldn't find it: {0}", filePath); - return null; - } - catch(Exception ex) + // Check .NET 8 compatibility before loading + var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath); + if (!isCompatible) { string fileName = Path.GetFileName(filePath); - - // Check if this might be a .NET Framework compatibility issue - if (ex.Message.Contains("Could not load type") || - ex.Message.Contains("Unable to load one or more of the requested types")) + Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); + + var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy); + IncompatiblePlugins.Add(incompatiblePlugin); + return null; + } + + var assembly = Assembly.LoadFrom(filePath); + if (assembly != null) + { + var assyVersion = GetAssemblyVersion(assembly); + var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly); + LoadedAssemblies.Add(loadedAssembly); + Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version); + return loadedAssembly; + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath); + } + return null; + } + catch (FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) + { + // Get the assembly name from the file path + string assemblyName = Path.GetFileNameWithoutExtension(filePath); + + // Try to find the already loaded assembly + var existingAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.OrdinalIgnoreCase)); + + if (existingAssembly != null) + { + Debug.LogMessage(LogEventLevel.Information, "Assembly '{0}' is already loaded, using existing instance", assemblyName); + var assyVersion = GetAssemblyVersion(existingAssembly); + var loadedAssembly = new LoadedAssembly(existingAssembly.GetName().Name, assyVersion, existingAssembly); + LoadedAssemblies.Add(loadedAssembly); + return loadedAssembly; + } + + Debug.LogMessage(LogEventLevel.Warning, "Assembly with same name already loaded but couldn't find it: {0}", filePath); + return null; + } + catch (Exception ex) + { + string fileName = Path.GetFileName(filePath); + + // Check if this might be a .NET Framework compatibility issue + if (ex.Message.Contains("Could not load type") || + ex.Message.Contains("Unable to load one or more of the requested types")) + { + Debug.LogMessage(LogEventLevel.Error, "Error loading assembly {0}: Likely .NET 8 compatibility issue: {1}", + fileName, ex.Message); + IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, ex.Message, requestedBy)); + } + else + { + Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath); + } + return null; + } + } + + /// + /// Retrieves the version information of the specified assembly. + /// + /// This method first attempts to retrieve the version from the . If the attribute is not present, it falls back to the assembly's + /// version as defined in its metadata. + /// The assembly from which to retrieve the version information. Cannot be . + /// A string representing the version of the assembly. If the assembly has an , its value is returned. Otherwise, the assembly's + /// version is returned in the format "Major.Minor.Build.Revision". + public static string GetAssemblyVersion(Assembly assembly) + { + var ver = assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); + if (ver != null && ver.Length > 0) + { + // Get the AssemblyInformationalVersion + AssemblyInformationalVersionAttribute verAttribute = ver[0] as AssemblyInformationalVersionAttribute; + return verAttribute.InformationalVersion; + } + else + { + // Get the AssemblyVersion + var version = assembly.GetName().Version; + var verStr = string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision); + return verStr; + } + } + + /// + /// Determines whether an assembly with the specified name is currently loaded. + /// + /// This method performs a case-sensitive comparison to determine if the specified assembly is + /// loaded. It logs verbose messages indicating the status of the check. + /// The name of the assembly to check. This value is case-sensitive. + /// if an assembly with the specified name is loaded; otherwise, . + public static bool CheckIfAssemblyLoaded(string name) + { + Debug.LogMessage(LogEventLevel.Verbose, "Checking if assembly: {0} is loaded...", name); + var loadedAssembly = LoadedAssemblies.FirstOrDefault(s => s.Name.Equals(name)); + + if (loadedAssembly != null) + { + Debug.LogMessage(LogEventLevel.Verbose, "Assembly already loaded."); + return true; + } + else + { + Debug.LogMessage(LogEventLevel.Verbose, "Assembly not loaded."); + return false; + } + } + + /// + /// Reports the versions of the Essentials framework, PepperDash Core, and loaded plugins to the console. + /// + /// This method outputs version information for the Essentials framework, PepperDash Core, and + /// all loaded Essentials plugins to the Crestron console. If any incompatible plugins are detected, their details + /// are also reported, including the reason for incompatibility and the plugin that required them, if + /// applicable. + /// The command string that triggered the version report. This parameter is not used directly by the method. + public static void ReportAssemblyVersions(string command) + { + CrestronConsole.ConsoleCommandResponse("Essentials Version: {0}" + CrestronEnvironment.NewLine, Global.AssemblyVersion); + CrestronConsole.ConsoleCommandResponse("PepperDash Core Version: {0}" + CrestronEnvironment.NewLine, PepperDashCoreAssembly.Version); + CrestronConsole.ConsoleCommandResponse("Essentials Plugin Versions:" + CrestronEnvironment.NewLine); + foreach (var assembly in EssentialsPluginAssemblies) + { + CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version); + } + + if (IncompatiblePlugins.Count > 0) + { + CrestronConsole.ConsoleCommandResponse("Incompatible Plugins:" + CrestronEnvironment.NewLine); + foreach (var plugin in IncompatiblePlugins) + { + if (plugin.TriggeredBy != "Direct load") { - Debug.LogMessage(LogEventLevel.Error, "Error loading assembly {0}: Likely .NET 8 compatibility issue: {1}", - fileName, ex.Message); - IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, ex.Message, requestedBy)); + CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine, + plugin.Name, plugin.Reason, plugin.TriggeredBy); } else { - Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath); + CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine, + plugin.Name, plugin.Reason); } - return null; + } + } + } + + /// + /// Moves .dll assemblies from the plugins folder to the loaded plugins directory. + /// + /// This method scans the plugins folder for .dll files and moves them to the loaded plugins + /// directory if they are not already loaded. If a file with the same name exists in the target directory, it is + /// replaced. The method logs the process at various stages and handles exceptions for individual files to ensure + /// the operation continues for other files. + private static void MoveDllAssemblies() + { + Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder..."); + + var pluginDi = new DirectoryInfo(PluginDirectory); + var pluginFiles = pluginDi.GetFiles("*.dll"); + + if (pluginFiles.Length > 0) + { + if (!Directory.Exists(LoadedPluginsDirectoryPath)) + { + Directory.CreateDirectory(LoadedPluginsDirectoryPath); } } - /// - /// Attempts to get the assembly informational version and if not possible gets the version - /// - /// - /// - public static string GetAssemblyVersion(Assembly assembly) + foreach (var pluginFile in pluginFiles) { - var ver = assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); - if (ver != null && ver.Length > 0) + try { - // Get the AssemblyInformationalVersion - AssemblyInformationalVersionAttribute verAttribute = ver[0] as AssemblyInformationalVersionAttribute; - return verAttribute.InformationalVersion; - } - else - { - // Get the AssemblyVersion - var version = assembly.GetName().Version; - var verStr = string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision); - return verStr; - } - } + Debug.LogMessage(LogEventLevel.Information, "Found .dll: {0}", pluginFile.Name); - /// - /// Checks if the filename matches an already loaded assembly file's name - /// - /// name of loaded assembly - /// True if file already matches loaded assembly file. - public static bool CheckIfAssemblyLoaded(string name) - { - Debug.LogMessage(LogEventLevel.Verbose, "Checking if assembly: {0} is loaded...", name); - var loadedAssembly = LoadedAssemblies.FirstOrDefault(s => s.Name.Equals(name)); - - if (loadedAssembly != null) - { - Debug.LogMessage(LogEventLevel.Verbose, "Assembly already loaded."); - return true; - } - else - { - Debug.LogMessage(LogEventLevel.Verbose, "Assembly not loaded."); - return false; - } - } - - /// - /// Associates the specified assembly with the given name in the loaded assemblies collection. - /// - /// If an assembly with the specified name already exists in the loaded assemblies collection, - /// this method updates its associated assembly. If no matching name is found, the method does nothing. - /// The name used to identify the assembly. This value is case-sensitive and must not be null or empty. - /// The assembly to associate with the specified name. This value must not be null. - public static void AddLoadedAssembly(string name, Assembly assembly) - { - var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name)); - - loadedAssembly?.SetAssembly(assembly); - } - - /// - /// Used by console command to report the currently loaded assemblies and versions - /// - /// - public static void ReportAssemblyVersions(string command) - { - CrestronConsole.ConsoleCommandResponse("Essentials Version: {0}" + CrestronEnvironment.NewLine, Global.AssemblyVersion); - CrestronConsole.ConsoleCommandResponse("PepperDash Core Version: {0}" + CrestronEnvironment.NewLine, PepperDashCoreAssembly.Version); - CrestronConsole.ConsoleCommandResponse("Essentials Plugin Versions:" + CrestronEnvironment.NewLine); - foreach (var assembly in EssentialsPluginAssemblies) - { - CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version); - } - - if (IncompatiblePlugins.Count > 0) - { - CrestronConsole.ConsoleCommandResponse("Incompatible Plugins:" + CrestronEnvironment.NewLine); - foreach (var plugin in IncompatiblePlugins) + if (!CheckIfAssemblyLoaded(pluginFile.Name)) { - if (plugin.TriggeredBy != "Direct load") + string filePath = string.Empty; + + filePath = LoadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name; + + // Check if there is a previous file in the loadedPlugins directory and delete + if (File.Exists(filePath)) { - CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine, - plugin.Name, plugin.Reason, plugin.TriggeredBy); - } - else - { - CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine, - plugin.Name, plugin.Reason); + Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath); + File.Delete(filePath); } + + // Move the file + File.Move(pluginFile.FullName, filePath); + Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath); } + else + { + Debug.LogMessage(LogEventLevel.Information, "Skipping assembly: {0}. There is already an assembly with that name loaded.", pluginFile.FullName); + } + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Verbose, "Error with plugin file {0} . Exception: {1}", pluginFile.FullName, e); + continue; //catching any load issues and continuing. There will be exceptions loading Crestron .dlls from the cplz Probably should do something different here } } - - /// - /// Moves any .dll assemblies not already loaded from the plugins folder to loadedPlugins folder - /// - static void MoveDllAssemblies() + + Debug.LogMessage(LogEventLevel.Information, "Done with .dll assemblies"); + } + + /// + /// Extracts and processes .cplz archive files found in the specified directories, moving their contents to the + /// appropriate plugin directory. + /// + /// This method searches for .cplz files in the plugin directory and user folder, extracts their + /// contents, and moves any .dll files to the loaded plugins directory. If a .dll file with the same name already + /// exists in the target directory, it is replaced with the new file. Temporary files and directories created during + /// the process are cleaned up after the operation completes. + private static void UnzipAndMoveCplzArchives() + { + Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder..."); + var di = new DirectoryInfo(PluginDirectory); + var zFiles = di.GetFiles("*.cplz"); + + //// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users + + //var userDi = new DirectoryInfo(Global.FilePathPrefix); + //var userZFiles = userDi.GetFiles("*.cplz"); + + Debug.LogInformation("Checking {folder} for .cplz files", Global.FilePathPrefix); + var cplzFiles = Directory.GetFiles(Global.FilePathPrefix, "*.cplz", SearchOption.AllDirectories) + .Select(f => new FileInfo(f)) + .ToArray(); + + if (cplzFiles.Length > 0) { - Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder..."); - - var pluginDi = new DirectoryInfo(_pluginDirectory); - var pluginFiles = pluginDi.GetFiles("*.dll"); - - if (pluginFiles.Length > 0) + if (!Directory.Exists(LoadedPluginsDirectoryPath)) { - if (!Directory.Exists(_loadedPluginsDirectoryPath)) - { - Directory.CreateDirectory(_loadedPluginsDirectoryPath); - } + Directory.CreateDirectory(LoadedPluginsDirectoryPath); } + } - foreach (var pluginFile in pluginFiles) + foreach (var zfi in cplzFiles) + { + Directory.CreateDirectory(TempDirectory); + var tempDi = new DirectoryInfo(TempDirectory); + + Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName); + var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName); + Debug.LogMessage(LogEventLevel.Information, "UnZip Result: {0}", result.ToString()); + + var tempFiles = tempDi.GetFiles("*.dll"); + foreach (var tempFile in tempFiles) { try { - Debug.LogMessage(LogEventLevel.Information, "Found .dll: {0}", pluginFile.Name); - - if (!CheckIfAssemblyLoaded(pluginFile.Name)) + if (!CheckIfAssemblyLoaded(tempFile.Name)) { string filePath = string.Empty; - filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name; + filePath = LoadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name; // Check if there is a previous file in the loadedPlugins directory and delete if (File.Exists(filePath)) @@ -446,488 +586,325 @@ namespace PepperDash.Essentials } // Move the file - File.Move(pluginFile.FullName, filePath); - Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath); + File.Move(tempFile.FullName, filePath); + Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath); } else { - Debug.LogMessage(LogEventLevel.Information, "Skipping assembly: {0}. There is already an assembly with that name loaded.", pluginFile.FullName); + Debug.LogMessage(LogEventLevel.Information, "Skipping assembly: {0}. There is already an assembly with that name loaded.", tempFile.FullName); } } catch (Exception e) { - Debug.LogMessage(LogEventLevel.Verbose, "Error with plugin file {0} . Exception: {1}", pluginFile.FullName, e); + Debug.LogMessage(LogEventLevel.Verbose, "Assembly {0} is not a custom assembly. Exception: {1}", tempFile.FullName, e); continue; //catching any load issues and continuing. There will be exceptions loading Crestron .dlls from the cplz Probably should do something different here } } - Debug.LogMessage(LogEventLevel.Information, "Done with .dll assemblies"); + // Delete the .cplz and the temp directory + Directory.Delete(TempDirectory, true); + zfi.Delete(); } - /// - /// Unzips each .cplz archive into the temp directory and moves any unloaded files into loadedPlugins - /// - static void UnzipAndMoveCplzArchives() + Debug.LogMessage(LogEventLevel.Information, "Done with .cplz archives"); + } + + /// + /// Loads plugin assemblies from the designated plugin directory, checks their compatibility with .NET 8, and loads + /// the compatible assemblies into the application. + /// + /// This method scans the plugin directory for all `.dll` files, verifies their compatibility + /// with .NET 8, and attempts to load the compatible assemblies. Assemblies that are incompatible are logged as + /// warnings and added to a list of incompatible plugins for further inspection. + private static void LoadPluginAssemblies() + { + Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder..."); + var pluginDi = new DirectoryInfo(LoadedPluginsDirectoryPath); + var pluginFiles = pluginDi.GetFiles("*.dll"); + + Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length); + + // First, check compatibility of all assemblies before loading any + var assemblyCompatibility = new Dictionary References)>(); + + foreach (var pluginFile in pluginFiles) { - Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder..."); - var di = new DirectoryInfo(_pluginDirectory); - var zFiles = di.GetFiles("*.cplz"); + string fileName = pluginFile.Name; + assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName); + } - //// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users + // Now load compatible assemblies and track incompatible ones + foreach (var pluginFile in pluginFiles) + { + string fileName = pluginFile.Name; + var (isCompatible, reason, _) = assemblyCompatibility[fileName]; - //var userDi = new DirectoryInfo(Global.FilePathPrefix); - //var userZFiles = userDi.GetFiles("*.cplz"); - - Debug.LogInformation("Checking {folder} for .cplz files", Global.FilePathPrefix); - var cplzFiles = Directory.GetFiles(Global.FilePathPrefix, "*.cplz", SearchOption.AllDirectories) - .Select(f => new FileInfo(f)) - .ToArray(); - - if (cplzFiles.Length > 0) + if (!isCompatible) { - if (!Directory.Exists(_loadedPluginsDirectoryPath)) - { - Directory.CreateDirectory(_loadedPluginsDirectoryPath); - } + Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); + IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, reason, null)); + continue; } - foreach (var zfi in cplzFiles) + // Try to load the assembly + var loadedAssembly = LoadAssembly(pluginFile.FullName, null); + + if (loadedAssembly != null) { - Directory.CreateDirectory(_tempDirectory); - var tempDi = new DirectoryInfo(_tempDirectory); + LoadedPluginFolderAssemblies.Add(loadedAssembly); + } + } - Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName); - var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName); - Debug.LogMessage(LogEventLevel.Information, "UnZip Result: {0}", result.ToString()); + Debug.LogMessage(LogEventLevel.Information, "All Plugins Loaded."); + } - var tempFiles = tempDi.GetFiles("*.dll"); - foreach (var tempFile in tempFiles) + /// + /// Loads and initializes custom plugin types from the assemblies in the plugin folder. + /// + /// This method iterates through all loaded plugin assemblies, identifies types that implement + /// the interface, and attempts to instantiate and load them. Assemblies or + /// types that cannot be loaded due to missing dependencies, type loading errors, or other exceptions are logged, + /// and incompatible plugins are tracked for further analysis. + private static void LoadCustomPluginTypes() + { + Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types..."); + + foreach (var loadedAssembly in LoadedPluginFolderAssemblies) + { + // Skip if assembly is null (can happen if we had loading issues) + if (loadedAssembly == null || loadedAssembly.Assembly == null) + continue; + + // iteratate this assembly's classes, looking for "LoadPlugin()" methods + try + { + var assy = loadedAssembly.Assembly; + Type[] types = []; + try + { + types = assy.GetTypes(); + Debug.LogMessage(LogEventLevel.Debug, $"Got types for assembly {assy.GetName().Name}"); + } + catch (ReflectionTypeLoadException e) + { + Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", + loadedAssembly.Name, e.Message); + + // Check if any of the loader exceptions are due to missing assemblies + foreach (var loaderEx in e.LoaderExceptions) + { + if (loaderEx is FileNotFoundException fileNotFoundEx) + { + string missingAssembly = fileNotFoundEx.FileName; + if (!string.IsNullOrEmpty(missingAssembly)) + { + Debug.LogMessage(LogEventLevel.Warning, "Assembly {0} requires missing dependency: {1}", + loadedAssembly.Name, missingAssembly); + + // Add to incompatible plugins with dependency information + IncompatiblePlugins.Add(new IncompatiblePlugin( + Path.GetFileName(missingAssembly), + $"Missing dependency required by {loadedAssembly.Name}", + loadedAssembly.Name)); + } + } + } + + Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); + continue; + } + catch (TypeLoadException e) + { + Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", + loadedAssembly.Name, e.Message); + Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); + + // Add to incompatible plugins if this is likely a .NET 8 compatibility issue + if (e.Message.Contains("Could not load type") || + e.Message.Contains("Unable to load one or more of the requested types")) + { + IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, + $"Type loading error: {e.Message}", + null)); + } + + continue; + } + foreach (var type in types) { try { - if (!CheckIfAssemblyLoaded(tempFile.Name)) + if (typeof(IPluginDeviceFactory).IsAssignableFrom(type) && !type.IsAbstract) { - string filePath = string.Empty; - - filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name; - - // Check if there is a previous file in the loadedPlugins directory and delete - if (File.Exists(filePath)) - { - Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath); - File.Delete(filePath); - } - - // Move the file - File.Move(tempFile.FullName, filePath); - Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Skipping assembly: {0}. There is already an assembly with that name loaded.", tempFile.FullName); + var plugin = + (IPluginDeviceFactory)Activator.CreateInstance(type); + LoadCustomPlugin(plugin, loadedAssembly); } } + catch (NotSupportedException) + { + //this happens for dlls that aren't PD dlls, like ports of Mono classes into S#. Swallowing. + } catch (Exception e) { - Debug.LogMessage(LogEventLevel.Verbose, "Assembly {0} is not a custom assembly. Exception: {1}", tempFile.FullName, e); - continue; //catching any load issues and continuing. There will be exceptions loading Crestron .dlls from the cplz Probably should do something different here - } - } - - // Delete the .cplz and the temp directory - Directory.Delete(_tempDirectory, true); - zfi.Delete(); - } - - Debug.LogMessage(LogEventLevel.Information, "Done with .cplz archives"); - } - - /// - /// Attempts to load the assemblies from the loadedPlugins folder - /// - static void LoadPluginAssemblies() - { - Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder..."); - var pluginDi = new DirectoryInfo(_loadedPluginsDirectoryPath); - var pluginFiles = pluginDi.GetFiles("*.dll"); - - Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length); - - // First, check compatibility of all assemblies before loading any - var assemblyCompatibility = new Dictionary References)>(); - - foreach (var pluginFile in pluginFiles) - { - string fileName = pluginFile.Name; - assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName); - } - - // Now load compatible assemblies and track incompatible ones - foreach (var pluginFile in pluginFiles) - { - string fileName = pluginFile.Name; - var (isCompatible, reason, references) = assemblyCompatibility[fileName]; - - if (!isCompatible) - { - Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); - IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, reason, null)); - continue; - } - - // Try to load the assembly - var loadedAssembly = LoadAssembly(pluginFile.FullName, null); - - if (loadedAssembly != null) - { - LoadedPluginFolderAssemblies.Add(loadedAssembly); - } - } - - Debug.LogMessage(LogEventLevel.Information, "All Plugins Loaded."); - } - - /// - /// Iterate the loaded assemblies and try to call the LoadPlugin method - /// - static void LoadCustomPluginTypes() - { - Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types..."); - - foreach (var loadedAssembly in LoadedPluginFolderAssemblies) - { - // Skip if assembly is null (can happen if we had loading issues) - if (loadedAssembly == null || loadedAssembly.Assembly == null) - continue; - - // iteratate this assembly's classes, looking for "LoadPlugin()" methods - try - { - var assy = loadedAssembly.Assembly; - Type[] types = { }; - try - { - types = assy.GetTypes(); - Debug.LogMessage(LogEventLevel.Debug, $"Got types for assembly {assy.GetName().Name}"); - } - catch (ReflectionTypeLoadException e) - { - Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", - loadedAssembly.Name, e.Message); - - // Check if any of the loader exceptions are due to missing assemblies - foreach (var loaderEx in e.LoaderExceptions) - { - if (loaderEx is FileNotFoundException fileNotFoundEx) - { - string missingAssembly = fileNotFoundEx.FileName; - if (!string.IsNullOrEmpty(missingAssembly)) - { - Debug.LogMessage(LogEventLevel.Warning, "Assembly {0} requires missing dependency: {1}", - loadedAssembly.Name, missingAssembly); - - // Add to incompatible plugins with dependency information - IncompatiblePlugins.Add(new IncompatiblePlugin( - Path.GetFileName(missingAssembly), - $"Missing dependency required by {loadedAssembly.Name}", - loadedAssembly.Name)); - } - } - } - - Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); + Debug.LogMessage(LogEventLevel.Error, "Load Plugin not found. {0}.{2} is not a plugin factory. Exception: {1}", + loadedAssembly.Name, e.Message, type.Name); continue; } - catch (TypeLoadException e) - { - Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", - loadedAssembly.Name, e.Message); - Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); - - // Add to incompatible plugins if this is likely a .NET 8 compatibility issue - if (e.Message.Contains("Could not load type") || - e.Message.Contains("Unable to load one or more of the requested types")) - { - IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, - $"Type loading error: {e.Message}", - null)); - } - - continue; - } - - foreach (var type in types) - { - try - { - if (typeof(IPluginDeviceFactory).IsAssignableFrom(type) && !type.IsAbstract) - { - var plugin = - (IPluginDeviceFactory)Activator.CreateInstance(type); - LoadCustomPlugin(plugin, loadedAssembly); - } - } - catch (NotSupportedException) - { - //this happens for dlls that aren't PD dlls, like ports of Mono classes into S#. Swallowing. - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Error, "Load Plugin not found. {0}.{2} is not a plugin factory. Exception: {1}", - loadedAssembly.Name, e.Message, type.Name); - continue; - } - } } - catch (Exception e) + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}", + loadedAssembly.Name, e.Message); + Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace); + + // Add to incompatible plugins if this is likely a .NET 8 compatibility issue + if (e.Message.Contains("Could not load type") || + e.Message.Contains("Unable to load one or more of the requested types")) { - Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}", - loadedAssembly.Name, e.Message); - Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace); - - // Add to incompatible plugins if this is likely a .NET 8 compatibility issue - if (e.Message.Contains("Could not load type") || - e.Message.Contains("Unable to load one or more of the requested types")) - { - IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, - $"Assembly loading error: {e.Message}", - null)); - } - - continue; + IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, + $"Assembly loading error: {e.Message}", + null)); } - } - - // Update incompatible plugins with dependency information - UpdateIncompatiblePluginDependencies(new Dictionary>()); - - // plugin dll will be loaded. Any classes in plugin should have a static constructor - // that registers that class with the Core.DeviceFactory - Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types."); - } - /// - /// Updates incompatible plugins with information about which plugins depend on them - /// - private static void UpdateIncompatiblePluginDependencies(Dictionary> pluginDependencies) - { - // For each incompatible plugin - foreach (var incompatiblePlugin in IncompatiblePlugins) - { - // If it already has a requestedBy, skip it - if (incompatiblePlugin.TriggeredBy != "Direct load") - continue; - - // Find plugins that depend on this incompatible plugin - foreach (var plugin in pluginDependencies) - { - string pluginName = plugin.Key; - List dependencies = plugin.Value; - - // If this plugin depends on the incompatible plugin - if (dependencies.Contains(incompatiblePlugin.Name) || - dependencies.Any(d => d.StartsWith(incompatiblePlugin.Name + ","))) - { - incompatiblePlugin.UpdateTriggeringPlugin(pluginName); - break; - } - } - } - } - /// - /// Loads a - /// - /// This method verifies that the plugin meets the minimum required Essentials framework version - /// before loading it. If the plugin fails the dependency check, it is skipped, and a log message is generated. If - /// the plugin passes the check, it is loaded, and its type factories are initialized. - /// The plugin to be loaded, implementing the interface. If the plugin also - /// implements , additional checks for development versions are - /// performed. - /// The assembly associated with the plugin being loaded. This is used for logging and tracking purposes. - static void LoadCustomPlugin(IPluginDeviceFactory pluginDeviceFactory, LoadedAssembly loadedAssembly) - { - var passed = pluginDeviceFactory is IPluginDevelopmentDeviceFactory developmentPlugin ? Global.IsRunningDevelopmentVersion - (developmentPlugin.DevelopmentEssentialsFrameworkVersions, developmentPlugin.MinimumEssentialsFrameworkVersion) - : Global.IsRunningMinimumVersionOrHigher(pluginDeviceFactory.MinimumEssentialsFrameworkVersion); - - if (!passed) - { - Debug.LogMessage(LogEventLevel.Information, - "\r\n********************\r\n\tPlugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin {1}\r\n********************", - pluginDeviceFactory.MinimumEssentialsFrameworkVersion, loadedAssembly.Name); - return; - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", pluginDeviceFactory.MinimumEssentialsFrameworkVersion); - } - - Debug.LogMessage(LogEventLevel.Information, "Loading plugin factory: {0}", loadedAssembly.Name); - - LoadDeviceFactories(pluginDeviceFactory); - - if (!EssentialsPluginAssemblies.Contains(loadedAssembly)) - EssentialsPluginAssemblies.Add(loadedAssembly); - } - - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IPluginDeviceFactory deviceFactory) - { - foreach (var typeName in deviceFactory.TypeNames) - { - string description = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0 - ? descriptionAttribute[0].Description - : "No description available"; - - DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); + continue; } } + // Update incompatible plugins with dependency information + var pluginDependencies = new Dictionary>(); + // Populate pluginDependencies with relevant data + // Example: pluginDependencies["PluginA"] = new List { "Dependency1", "Dependency2" }; + UpdateIncompatiblePluginDependencies(pluginDependencies); - /// - /// LoadPlugins method - /// - public static void LoadPlugins() + // plugin dll will be loaded. Any classes in plugin should have a static constructor + // that registers that class with the Core.DeviceFactory + Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types."); + } + + /// + /// Updates the triggering plugin information for incompatible plugins based on their dependencies. + /// + /// This method iterates through a predefined list of incompatible plugins and updates their + /// triggering plugin information if they were directly loaded and are found to be dependencies of other plugins. + /// The update is performed for the first plugin that depends on the incompatible plugin. + /// A dictionary where the key is the name of a plugin and the value is a list of its dependencies. Each dependency + /// is represented as a string, which may include additional metadata. + private static void UpdateIncompatiblePluginDependencies(Dictionary> pluginDependencies) + { + // For each incompatible plugin + foreach (var incompatiblePlugin in IncompatiblePlugins) { - Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory); + // If it already has a requestedBy, skip it + if (incompatiblePlugin.TriggeredBy != "Direct load") + continue; - if (Directory.Exists(_pluginDirectory)) + // Find plugins that depend on this incompatible plugin + foreach (var plugin in pluginDependencies) { - Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins"); + string pluginName = plugin.Key; + List dependencies = plugin.Value; - // Deal with any .dll files - MoveDllAssemblies(); - - // Deal with any .cplz files - UnzipAndMoveCplzArchives(); - - if (Directory.Exists(_loadedPluginsDirectoryPath)) + // If this plugin depends on the incompatible plugin + if (dependencies.Contains(incompatiblePlugin.Name) || + dependencies.Any(d => d.StartsWith(incompatiblePlugin.Name + ","))) { - // Load the assemblies from the loadedPlugins folder into the AppDomain - LoadPluginAssemblies(); - - // Load the types from any custom plugin assemblies - LoadCustomPluginTypes(); - } - - // Report on incompatible plugins - if (IncompatiblePlugins.Count > 0) - { - Debug.LogMessage(LogEventLevel.Warning, "Found {0} incompatible plugins:", IncompatiblePlugins.Count); - foreach (var plugin in IncompatiblePlugins) - { - if (plugin.TriggeredBy != "Direct load") - { - Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})", - plugin.Name, plugin.Reason, plugin.TriggeredBy); - } - else - { - Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}", - plugin.Name, plugin.Reason); - } - } + incompatiblePlugin.UpdateTriggeringPlugin(pluginName); + break; } } } } /// - /// Represents a LoadedAssembly + /// Loads a custom plugin and performs a dependency check to ensure compatibility with the required Essentials + /// framework version. /// - public class LoadedAssembly + /// This method verifies that the plugin meets the minimum required Essentials framework version + /// before loading it. If the plugin fails the dependency check, it is skipped, and a log message is generated. If + /// the plugin passes the check, it is loaded, and its type factories are initialized. + /// The plugin to be loaded, implementing the interface. If the plugin also + /// implements , additional checks for development versions are + /// performed. + /// The assembly associated with the plugin being loaded. This is used for logging and tracking purposes. + private static void LoadCustomPlugin(IPluginDeviceFactory plugin, LoadedAssembly loadedAssembly) { - /// - /// Gets the name of the assembly - /// - [JsonProperty("name")] - public string Name { get; private set; } + var passed = plugin is IPluginDevelopmentDeviceFactory developmentPlugin ? Global.IsRunningDevelopmentVersion + (developmentPlugin.DevelopmentEssentialsFrameworkVersions, developmentPlugin.MinimumEssentialsFrameworkVersion) + : Global.IsRunningMinimumVersionOrHigher(plugin.MinimumEssentialsFrameworkVersion); - /// - /// Gets the version of the assembly - /// - [JsonProperty("version")] - public string Version { get; private set; } - - /// - /// Gets the assembly - /// - [JsonIgnore] - public Assembly Assembly { get; private set; } - - /// - /// Initializes a new instance of the LoadedAssembly class - /// - /// The name of the assembly - /// The version of the assembly - /// The assembly - public LoadedAssembly(string name, string version, Assembly assembly) + if (!passed) { - Name = name; - Version = version; - Assembly = assembly; + Debug.LogMessage(LogEventLevel.Information, + "\r\n********************\r\n\tPlugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin {1}\r\n********************", + plugin.MinimumEssentialsFrameworkVersion, loadedAssembly.Name); + return; + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", plugin.MinimumEssentialsFrameworkVersion); } - /// - /// SetAssembly method - /// - public void SetAssembly(Assembly assembly) - { - Assembly = assembly; - } + Debug.LogMessage(LogEventLevel.Information, "Loading plugin: {0}", loadedAssembly.Name); + + LoadDeviceFactories(deviceFactory); + + if (!EssentialsPluginAssemblies.Contains(loadedAssembly)) + EssentialsPluginAssemblies.Add(loadedAssembly); } - + /// - /// Represents a plugin that was found to be incompatible with .NET 8 + /// Loads plugins from the designated plugin directory, processes them, and integrates them into the application. /// - public class IncompatiblePlugin + /// This method performs the following steps: Checks if + /// the plugin directory exists. Processes any plugin files, including .dll + /// and .cplz files, by moving or extracting them as needed. Loads + /// assemblies from the processed plugins into the application domain. + /// Identifies and reports any incompatible plugins, including the reason for + /// incompatibility. Plugins that are successfully loaded are made available for use, + /// while incompatible plugins are logged for review. + public static void LoadPlugins() { - [JsonProperty("name")] - public string Name { get; private set; } - - [JsonProperty("reason")] - public string Reason { get; private set; } - - [JsonProperty("triggeredBy")] - public string TriggeredBy { get; private set; } - - public IncompatiblePlugin(string name, string reason, string triggeredBy = null) + Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", PluginDirectory); + + if (Directory.Exists(PluginDirectory)) { - Name = name; - Reason = reason; - TriggeredBy = triggeredBy ?? "Direct load"; - } - - /// - /// Updates the plugin that triggered this incompatibility - /// - /// Name of the plugin that requires this incompatible plugin - public void UpdateTriggeringPlugin(string triggeringPlugin) - { - if (!string.IsNullOrEmpty(triggeringPlugin)) + Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins"); + + // Deal with any .dll files + MoveDllAssemblies(); + + // Deal with any .cplz files + UnzipAndMoveCplzArchives(); + + if (Directory.Exists(LoadedPluginsDirectoryPath)) { - TriggeredBy = triggeringPlugin; + // Load the assemblies from the loadedPlugins folder into the AppDomain + LoadPluginAssemblies(); + + // Load the types from any custom plugin assemblies + LoadCustomPluginTypes(); + } + + // Report on incompatible plugins + if (IncompatiblePlugins.Count > 0) + { + Debug.LogMessage(LogEventLevel.Warning, "Found {0} incompatible plugins:", IncompatiblePlugins.Count); + foreach (var plugin in IncompatiblePlugins) + { + if (plugin.TriggeredBy != "Direct load") + { + Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})", + plugin.Name, plugin.Reason, plugin.TriggeredBy); + } + else + { + Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}", + plugin.Name, plugin.Reason); + } + } } } } - - /// - /// Attribute to explicitly mark a plugin as .NET 8 compatible - /// - [AttributeUsage(AttributeTargets.Assembly)] - public class Net8CompatibleAttribute : Attribute - { - public bool IsCompatible { get; } - - public Net8CompatibleAttribute(bool isCompatible = true) - { - IsCompatible = isCompatible; - } - } -} \ No newline at end of file +} From aaa5b0532be35777a5f8b8c823cba35222093f2b Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 13:33:56 -0500 Subject: [PATCH 012/161] feat: modify plugin loading process --- .../Devices/ConfigSnippetAttribute.cs | 27 ------ .../Devices/DescriptionAttribute.cs | 26 ------ .../Devices/EssentialsDevice.cs | 86 +++++++++++++++++++ .../Devices/EssentialsDeviceFactory.cs | 41 +++++---- ...ssentialsPluginDevelopmentDeviceFactory.cs | 22 ----- .../Devices/EssentialsPluginDeviceFactory.cs | 14 --- .../ProcessorExtensionDeviceFactory.cs | 47 ---------- .../Factory/IDeviceFactory.cs | 4 +- .../ProcessorExtensionDeviceFactory.cs | 2 +- .../Plugins/IPluginDeviceFactory.cs | 18 +--- .../Plugins/PluginLoader.cs | 43 +++++++--- .../DeviceFactory.cs | 2 +- .../MobileControlFactory.cs | 2 +- .../Factory/DeviceFactory.cs | 2 +- 14 files changed, 147 insertions(+), 189 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs delete mode 100644 src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs delete mode 100644 src/PepperDash.Essentials.Core/Devices/EssentialsPluginDevelopmentDeviceFactory.cs delete mode 100644 src/PepperDash.Essentials.Core/Devices/EssentialsPluginDeviceFactory.cs delete mode 100644 src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs diff --git a/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs b/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs deleted file mode 100644 index a1fdef35..00000000 --- a/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a ConfigSnippetAttribute - /// - [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] - public class ConfigSnippetAttribute : Attribute - { - /// - /// Represents a configuration snippet for the device. - /// - /// - public ConfigSnippetAttribute(string configSnippet) - { - ConfigSnippet = configSnippet; - } - - /// - /// Gets the configuration snippet for the device. - /// This snippet can be used in the DeviceConfig to instantiate the device. - /// - public string ConfigSnippet { get; } - } - -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs b/src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs deleted file mode 100644 index abe51665..00000000 --- a/src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a description attribute for a device. - /// - [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] - public class DescriptionAttribute : Attribute - { - /// - /// Represents a description attribute for a device. - /// - /// - public DescriptionAttribute(string description) - { - Description = description; - } - - /// - /// Gets the description for the device. - /// - public string Description { get; } - } - -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs index 59fba681..41b46c29 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using PepperDash.Core; +using PepperDash.Essentials.Core.Config; using Serilog.Events; namespace PepperDash.Essentials.Core @@ -100,4 +102,88 @@ namespace PepperDash.Essentials.Core } } + + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] + public class DescriptionAttribute : Attribute + { + private string _Description; + + public DescriptionAttribute(string description) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Setting Description: {0}", description); + _Description = description; + } + + public string Description + { + get { return _Description; } + } + } + + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] + public class ConfigSnippetAttribute : Attribute + { + private string _ConfigSnippet; + + public ConfigSnippetAttribute(string configSnippet) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Setting Config Snippet {0}", configSnippet); + _ConfigSnippet = configSnippet; + } + + public string ConfigSnippet + { + get { return _ConfigSnippet; } + } + } + + /// + /// Represents a factory for creating processor extension devices. + /// + /// The type of the processor extension device. + public abstract class ProcessorExtensionDeviceFactory : IProcessorExtensionDeviceFactory where T : EssentialsDevice + { + #region IProcessorExtensionDeviceFactory Members + + /// + /// A list of strings that can be used in the type property of a DeviceConfig object to build an instance of this device + /// + public List TypeNames { get; protected set; } + + /// + /// Loads an item to the ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods dictionary for each entry in the TypeNames list + /// + public void LoadFactories() + { + foreach (var typeName in TypeNames) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = typeof(T).GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = typeof(T).GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + ProcessorExtensionDeviceFactory.AddFactoryForType(typeName.ToLower(), description, typeof(T), BuildDevice); + } + } + + /// + /// The method that will build the device + /// + /// The device config + /// An instance of the device + public abstract EssentialsDevice BuildDevice(DeviceConfig dc); + + #endregion + + } + + /// + /// Devices the basic needs for a Device Factory + /// + public abstract class EssentialsPluginDeviceFactory : EssentialsDeviceFactory, IPluginDeviceFactory where T : EssentialsDevice + { + /// + /// Specifies the minimum version of Essentials required for a plugin to run. Must use the format Major.Minor.Build (ex. "1.4.33") + /// + public string MinimumEssentialsFrameworkVersion { get; protected set; } + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs index 9c1a6a94..bf635c0c 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs @@ -1,27 +1,32 @@ -using System; +using System; using System.Collections.Generic; using PepperDash.Essentials.Core.Config; namespace PepperDash.Essentials.Core { - /// - /// Provides the basic needs for a Device Factory - /// - public abstract class EssentialsDeviceFactory : IDeviceFactory where T : EssentialsDevice - { - /// - public Type FactoryType => typeof(T); - /// - /// A list of strings that can be used in the type property of a DeviceConfig object to build an instance of this device + /// Devices the basic needs for a Device Factory /// - public List TypeNames { get; protected set; } + public abstract class EssentialsDeviceFactory : IDeviceFactory where T:EssentialsDevice + { + #region IDeviceFactory Members + + /// + public Type FactoryType => typeof(T); + + /// + /// A list of strings that can be used in the type property of a DeviceConfig object to build an instance of this device + /// + public List TypeNames { get; protected set; } + + /// + /// The method that will build the device + /// + /// The device config + /// An instance of the device + public abstract EssentialsDevice BuildDevice(DeviceConfig dc); + + #endregion + } - /// - /// Build the device using the configuration - /// - /// The device config - /// An instance of the device - public abstract EssentialsDevice BuildDevice(DeviceConfig dc); - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDevelopmentDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDevelopmentDeviceFactory.cs deleted file mode 100644 index 8bd60124..00000000 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDevelopmentDeviceFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; - -namespace PepperDash.Essentials.Core -{ - /// - /// EssentialsPluginDevelopmentDeviceFactory class - /// - /// - public abstract class EssentialsPluginDevelopmentDeviceFactory : EssentialsDeviceFactory, IPluginDevelopmentDeviceFactory where T : EssentialsDevice - { - /// - /// Specifies the minimum version of Essentials required for a plugin to run. Must use the format Major.Minor.Build (ex. "1.4.33") - /// - public string MinimumEssentialsFrameworkVersion { get; protected set; } - - /// - /// Gets or sets the DevelopmentEssentialsFrameworkVersions - /// - public List DevelopmentEssentialsFrameworkVersions { get; protected set; } - } - -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDeviceFactory.cs deleted file mode 100644 index 7a891905..00000000 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDeviceFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PepperDash.Essentials.Core -{ - /// - /// Devices the basic needs for a Device Factory - /// - public abstract class EssentialsPluginDeviceFactory : EssentialsDeviceFactory, IPluginDeviceFactory where T : EssentialsDevice - { - /// - /// Specifies the minimum version of Essentials required for a plugin to run. Must use the format Major.Minor.Build (ex. "1.4.33") - /// - public string MinimumEssentialsFrameworkVersion { get; protected set; } - } - -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs deleted file mode 100644 index 8549fd38..00000000 --- a/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using PepperDash.Essentials.Core.Config; - -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a factory for creating processor extension devices. - /// - /// The type of the processor extension device. - [Obsolete("will be removed in a future version")] - public abstract class ProcessorExtensionDeviceFactory : IProcessorExtensionDeviceFactory where T : EssentialsDevice - { - #region IProcessorExtensionDeviceFactory Members - - /// - /// Gets or sets the TypeNames - /// - public List TypeNames { get; protected set; } - - /// - /// LoadFactories method - /// - public void LoadFactories() - { - foreach (var typeName in TypeNames) - { - string description = typeof(T).GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0 - ? descriptionAttribute[0].Description - : "No description available"; - - ProcessorExtensionDeviceFactory.AddFactoryForType(typeName.ToLower(), description, typeof(T), BuildDevice); - } - } - - /// - /// The method that will build the device - /// - /// The device config - /// An instance of the device - public abstract EssentialsDevice BuildDevice(DeviceConfig dc); - - #endregion - - } - -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs index b267edf6..f1a0923e 100644 --- a/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs @@ -1,6 +1,6 @@ -using System; +using PepperDash.Essentials.Core.Config; +using System; using System.Collections.Generic; -using PepperDash.Essentials.Core.Config; namespace PepperDash.Essentials.Core { diff --git a/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs index 6336795c..1d9f41e1 100644 --- a/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs @@ -20,7 +20,7 @@ namespace PepperDash.Essentials.Core /// public ProcessorExtensionDeviceFactory() { var assy = Assembly.GetExecutingAssembly(); - PluginLoader.SetEssentialsAssembly(assy.GetName().Name, assy); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); var extensions = assy.GetTypes().Where(ct => typeof(IProcessorExtensionDeviceFactory) .IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); diff --git a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs index 72b6ad7b..caee2dcd 100644 --- a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs @@ -1,4 +1,4 @@ -using System; +using PepperDash.Essentials.Core.Config; using System.Collections.Generic; @@ -8,25 +8,11 @@ namespace PepperDash.Essentials.Core /// Defines a class that is capable of loading custom plugin device types /// public interface IPluginDeviceFactory : IDeviceFactory - { + { /// /// Required to define the minimum version for Essentials in the format xx.yy.zz /// string MinimumEssentialsFrameworkVersion { get; } - - } - - /// - /// Defines a factory for creating plugin development devices, including support for specific framework versions. - /// - /// This interface extends to provide additional functionality - /// specific to plugin development environments. - public interface IPluginDevelopmentDeviceFactory : IPluginDeviceFactory - { - /// - /// Gets a list of Essentials versions that this device is compatible with. - /// - List DevelopmentEssentialsFrameworkVersions { get; } } } diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index 6a857682..49ebad46 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -198,7 +198,7 @@ public static class PluginLoader /// this method updates its associated assembly. If no matching name is found, the method does nothing. /// The name used to identify the assembly. This value is case-sensitive and must not be null or empty. /// The assembly to associate with the specified name. This value must not be null. - public static void SetEssentialsAssembly(string name, Assembly assembly) + public static void AddLoadedAssembly(string name, Assembly assembly) { var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name)); @@ -816,7 +816,7 @@ public static class PluginLoader } } } - + /// /// Loads a custom plugin and performs a dependency check to ensure compatibility with the required Essentials /// framework version. @@ -824,36 +824,53 @@ public static class PluginLoader /// This method verifies that the plugin meets the minimum required Essentials framework version /// before loading it. If the plugin fails the dependency check, it is skipped, and a log message is generated. If /// the plugin passes the check, it is loaded, and its type factories are initialized. - /// The plugin to be loaded, implementing the interface. If the plugin also + /// The plugin to be loaded, implementing the interface. If the plugin also /// implements , additional checks for development versions are /// performed. /// The assembly associated with the plugin being loaded. This is used for logging and tracking purposes. - private static void LoadCustomPlugin(IPluginDeviceFactory plugin, LoadedAssembly loadedAssembly) + private static void LoadCustomPlugin(IPluginDeviceFactory deviceFactory, LoadedAssembly loadedAssembly) { - var passed = plugin is IPluginDevelopmentDeviceFactory developmentPlugin ? Global.IsRunningDevelopmentVersion - (developmentPlugin.DevelopmentEssentialsFrameworkVersions, developmentPlugin.MinimumEssentialsFrameworkVersion) - : Global.IsRunningMinimumVersionOrHigher(plugin.MinimumEssentialsFrameworkVersion); + var passed = Global.IsRunningMinimumVersionOrHigher(deviceFactory.MinimumEssentialsFrameworkVersion); if (!passed) { - Debug.LogMessage(LogEventLevel.Information, - "\r\n********************\r\n\tPlugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin {1}\r\n********************", - plugin.MinimumEssentialsFrameworkVersion, loadedAssembly.Name); + Debug.LogInformation( + "\r\n********************\r\n\tPlugin indicates minimum Essentials version {minimumEssentialsVersion}. Dependency check failed. Skipping Plugin {pluginName}\r\n********************", + deviceFactory.MinimumEssentialsFrameworkVersion, loadedAssembly.Name); return; } else { - Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", plugin.MinimumEssentialsFrameworkVersion); + Debug.LogInformation("Passed plugin passed dependency check (required version {essentialsMinimumVersion})", deviceFactory.MinimumEssentialsFrameworkVersion); } - Debug.LogMessage(LogEventLevel.Information, "Loading plugin: {0}", loadedAssembly.Name); - + Debug.LogInformation("Loading plugin: {pluginName}", loadedAssembly.Name); + LoadDeviceFactories(deviceFactory); if (!EssentialsPluginAssemblies.Contains(loadedAssembly)) EssentialsPluginAssemblies.Add(loadedAssembly); } + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IPluginDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); + } + } + /// /// Loads plugins from the designated plugin directory, processes them, and integrates them into the application. /// diff --git a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs index 2c7507ba..3a3117cb 100644 --- a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs @@ -20,7 +20,7 @@ namespace PepperDash.Essentials.Devices.Common public DeviceFactory() { var assy = Assembly.GetExecutingAssembly(); - PluginLoader.SetEssentialsAssembly(assy.GetName().Name, assy); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index 4f3c5433..9ce584f3 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -18,7 +18,7 @@ namespace PepperDash.Essentials { var assembly = Assembly.GetExecutingAssembly(); - PluginLoader.SetEssentialsAssembly(assembly.GetName().Name, assembly); + PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly); var types = assembly.GetTypes().Where(t => typeof(IDeviceFactory).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); diff --git a/src/PepperDash.Essentials/Factory/DeviceFactory.cs b/src/PepperDash.Essentials/Factory/DeviceFactory.cs index e60b3fa4..36fca96a 100644 --- a/src/PepperDash.Essentials/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials/Factory/DeviceFactory.cs @@ -21,7 +21,7 @@ namespace PepperDash.Essentials public DeviceFactory() { var assy = Assembly.GetExecutingAssembly(); - PluginLoader.SetEssentialsAssembly(assy.GetName().Name, assy); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); From 3ece4f0b7bedb64d85cf51b8217bc5dd09968b92 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 16:02:32 -0500 Subject: [PATCH 013/161] chore: move all files to file-scoped namespace --- src/PepperDash.Core/ComTextHelper.cs | 43 - .../Comm/CommunicationGather.cs | 85 +- .../Comm/CommunicationStreamDebugging.cs | 269 +- .../Comm/ControlPropertiesConfig.cs | 141 +- src/PepperDash.Core/Comm/EventArgs.cs | 379 +- .../Comm/GenericSecureTcpIpClient.cs | 1745 ++++---- .../GenericSecureTcpIpClient_ForServer.cs | 1613 ++++---- .../Comm/GenericSecureTcpIpServer.cs | 1739 ++++---- src/PepperDash.Core/Comm/GenericSshClient.cs | 1090 +++-- .../Comm/GenericTcpIpClient.cs | 807 ++-- .../Comm/GenericTcpIpClient_ForServer.cs | 1366 ++++--- .../Comm/GenericTcpIpServer.cs | 1683 ++++---- src/PepperDash.Core/Comm/GenericUdpServer.cs | 706 ++-- .../Comm/TcpClientConfigObject.cs | 91 +- .../Comm/TcpServerConfigObject.cs | 99 +- src/PepperDash.Core/Comm/eControlMethods.cs | 153 +- .../Comm/eStreamDebuggingSetting.cs | 28 - src/PepperDash.Core/CommunicationExtras.cs | 322 +- .../Config/PortalConfigReader.cs | 91 +- src/PepperDash.Core/Conversion/Convert.cs | 32 +- src/PepperDash.Core/CoreInterfaces.cs | 42 +- src/PepperDash.Core/Device.cs | 352 +- src/PepperDash.Core/EthernetHelper.cs | 13 +- src/PepperDash.Core/EventArgs.cs | 7 +- .../GenericRESTfulCommunications/Constants.cs | 9 +- .../GenericRESTfulClient.cs | 13 +- .../EventArgs and Constants.cs | 7 +- .../JsonStandardObjects/JsonToSimplDevice.cs | 7 +- .../JsonToSimplDeviceConfig.cs | 205 +- src/PepperDash.Core/JsonToSimpl/Constants.cs | 165 +- src/PepperDash.Core/JsonToSimpl/Global.cs | 18 +- .../JsonToSimplArrayLookupChild.cs | 32 +- .../JsonToSimpl/JsonToSimplChildObjectBase.cs | 209 +- .../JsonToSimpl/JsonToSimplFileMaster.cs | 482 ++- .../JsonToSimpl/JsonToSimplFixedPathObject.cs | 19 +- .../JsonToSimpl/JsonToSimplGenericMaster.cs | 31 +- .../JsonToSimpl/JsonToSimplMaster.cs | 92 +- .../JsonToSimplPortalFileMaster.cs | 31 +- .../Logging/CrestronEnricher.cs | 49 +- src/PepperDash.Core/Logging/Debug.cs | 5 +- .../Logging/DebugConsoleSink.cs | 67 +- src/PepperDash.Core/Logging/DebugContext.cs | 497 ++- .../Logging/DebugCrestronLoggerSink.cs | 39 +- .../Logging/DebugErrorLogSink.cs | 87 +- .../Logging/DebugExtensions.cs | 146 +- src/PepperDash.Core/Logging/DebugMemory.cs | 111 +- .../Logging/DebugWebsocketSink.cs | 4 - .../Network/DiscoveryThings.cs | 18 +- .../PasswordManagement/Config.cs | 7 +- .../PasswordManagement/Constants.cs | 7 +- .../PasswordManagement/PasswordClient.cs | 13 +- .../PasswordManagement/PasswordManager.cs | 15 +- .../SystemInfo/EventArgs and Constants.cs | 103 +- .../SystemInfo/SystemInfoConfig.cs | 211 +- .../SystemInfo/SystemInfoToSimpl.cs | 49 +- src/PepperDash.Core/Web/BouncyCertificate.cs | 650 ++- .../RequestHandlers/DefaultRequestHandler.cs | 13 +- .../WebApiBaseRequestAsyncHandler.cs | 292 +- .../WebApiBaseRequestHandler.cs | 7 +- src/PepperDash.Core/Web/WebApiServer.cs | 7 +- src/PepperDash.Core/WebApi/Presets/Preset.cs | 93 +- src/PepperDash.Core/WebApi/Presets/User.cs | 83 +- .../WebApi/Presets/WebApiPasscodeClient.cs | 194 +- .../Serialization/IXSigSerialization.cs | 33 +- .../XSigSerializationException.cs | 39 +- .../XSigUtility/Tokens/XSigAnalogToken.cs | 155 +- .../XSigUtility/Tokens/XSigDigitalToken.cs | 149 +- .../XSigUtility/Tokens/XSigSerialToken.cs | 136 +- .../XSigUtility/Tokens/XSigToken.cs | 77 +- .../XSigUtility/Tokens/XSigTokenType.cs | 33 +- .../XSigUtility/XSigHelpers.cs | 461 +-- .../XSigUtility/XSigTokenStreamReader.cs | 248 +- .../XSigUtility/XSigTokenStreamWriter.cs | 246 +- .../Bridges/BridgeBase.cs | 811 ++-- .../Bridges/BridgeHelper.cs | 107 +- .../Bridges/IBridge.cs | 22 +- .../JoinMaps/AirMediaControllerJoinMap.cs | 164 +- .../Bridges/JoinMaps/AppleTvJoinMap.cs | 111 +- .../JoinMaps/C2nRthsControllerJoinMap.cs | 89 +- .../JoinMaps/CameraControllerJoinMap.cs | 192 +- .../CenOdtOccupancySensorBaseJoinMap.cs | 498 +-- .../JoinMaps/DisplayControllerJoinMap.cs | 199 +- .../DmBladeChassisControllerJoinMap.cs | 174 +- .../JoinMaps/DmChassisControllerJoinMap.cs | 410 +- .../JoinMaps/DmRmcControllerJoinMap.cs | 216 +- .../Bridges/JoinMaps/DmTxControllerJoinMap.cs | 227 +- .../DmpsAudioOutputControllerJoinMap.cs | 443 +-- .../DmpsMicrophoneControllerJoinMap.cs | 103 +- .../JoinMaps/DmpsRoutingControllerJoinMap.cs | 289 +- .../JoinMaps/GenericIrControllerJoinMap.cs | 10 +- .../JoinMaps/GenericLightingJoinMap.cs | 95 +- .../JoinMaps/GenericRelayControllerJoinMap.cs | 49 +- .../JoinMaps/GlsOccupancySensorBaseJoinMap.cs | 474 +-- .../JoinMaps/GlsPartitionSensorJoinMap.cs | 315 +- .../JoinMaps/HdMdNxM4kEControllerJoinMap.cs | 146 +- .../JoinMaps/HdMdxxxCEControllerJoinMap.cs | 171 +- .../JoinMaps/HdPsXxxControllerJoinMap.cs | 10 +- .../Hrxxx0WirelessRemoteControllerJoinMap.cs | 641 ++- .../Bridges/JoinMaps/IAnalogInputJoinMap.cs | 59 +- .../JoinMaps/IBasicCommunicationJoinMap.cs | 102 +- .../Bridges/JoinMaps/IDigitalInputJoinMap.cs | 48 +- .../Bridges/JoinMaps/IDigitalOutputJoinMap.cs | 48 +- .../Bridges/JoinMaps/IRBlurayBaseJoinMap.cs | 559 +-- .../Bridges/JoinMaps/PduJoinMapBase.cs | 130 +- .../JoinMaps/SetTopBoxControllerJoinMap.cs | 621 ++- .../JoinMaps/StatusSignControllerJoinMap.cs | 128 +- .../Bridges/JoinMaps/SystemMonitorJoinMap.cs | 330 +- .../JoinMaps/VideoCodecControllerJoinMap.cs | 1352 +++---- .../Comm and IR/CecPortController.cs | 246 +- .../Comm and IR/ComPortController.cs | 148 +- .../Comm and IR/ComSpecJsonConverter.cs | 176 +- .../Comm and IR/CommFactory.cs | 224 +- .../Comm and IR/CommunicationExtras.cs | 7 +- .../Comm and IR/ConsoleCommMockDevice.cs | 43 +- .../Comm and IR/GenericComm.cs | 232 +- .../Comm and IR/GenericHttpClient.cs | 27 +- .../Comm and IR/IRPortHelper.cs | 121 +- .../Config/AudioControlPointListItem.cs | 24 +- .../Config/BasicConfig.cs | 160 +- .../Config/ConfigPropertiesHelpers.cs | 30 +- .../Config/DeviceConfig.cs | 153 +- .../Config/Essentials/ConfigReader.cs | 259 +- .../Config/Essentials/ConfigUpdater.cs | 367 +- .../Config/Essentials/ConfigWriter.cs | 268 +- .../Config/Essentials/EssentialsConfig.cs | 274 +- .../Config/ILoadConfig.cs | 15 +- .../Config/InfoConfig.cs | 100 +- .../SourceDevicePropertiesConfigBase.cs | 18 +- .../Crestron/CrestronGenericBaseDevice.cs | 223 +- .../CrestronIO/GenericRelayDevice.cs | 424 +- .../GenericVersiportAnalogInputDevice.cs | 4 +- .../CrestronIO/GenericVersiportInputDevice.cs | 4 +- .../GenericVersiportOutputDevice.cs | 243 +- .../CrestronIO/IAnalogInput.cs | 4 +- .../CrestronIO/IDigitalInput.cs | 25 +- .../CrestronIO/IDigitalOutput.cs | 7 +- .../CrestronIO/IHasCresnetBranches.cs | 7 +- .../CrestronIO/IOPortConfig.cs | 1 - .../CrestronIO/ISwitchedOutput.cs | 53 +- .../Device Info/DeviceInfo.cs | 51 +- .../Device Info/DeviceInfoEventArgs.cs | 48 +- .../Device Info/IDeviceInfoProvider.cs | 44 +- .../Device Info/NetworkDeviceHelpers.cs | 368 +- .../DeviceTypeInterfaces/IChannel.cs | 7 +- .../DeviceTypeInterfaces/IColorFunctions.cs | 7 +- .../DeviceTypeInterfaces/IDPad.cs | 7 +- .../IDiscPlayerControls.cs | 7 +- .../DeviceTypeInterfaces/IDisplay.cs | 21 +- .../DeviceTypeInterfaces/IDisplayBasic.cs | 6 +- .../DeviceTypeInterfaces/IDumbSource.cs | 13 +- .../DeviceTypeInterfaces/IDvr.cs | 7 +- .../DeviceTypeInterfaces/IEmergencyOSD.cs | 33 +- .../DeviceTypeInterfaces/IHasBranding.cs | 29 +- .../IHasFarEndContentStatus.cs | 20 +- .../DeviceTypeInterfaces/IHasInputs.cs | 27 +- .../DeviceTypeInterfaces/IHasPhoneDialing.cs | 65 +- .../IHasScreensWithLayouts.cs | 185 +- .../IHasSurroundSoundModes.cs | 5 - .../DeviceTypeInterfaces/IHasWebView.cs | 143 +- .../DeviceTypeInterfaces/IHumiditySensor.cs | 7 +- .../ILanguageDefinition.cs | 83 +- .../DeviceTypeInterfaces/ILanguageProvider.cs | 29 +- .../DeviceTypeInterfaces/ILevelControls.cs | 19 +- .../DeviceTypeInterfaces/IMobileControl.cs | 111 +- .../DeviceTypeInterfaces/IPasswordPrompt.cs | 101 +- .../DeviceTypeInterfaces/IPower.cs | 131 +- .../IProjectorScreenLiftControl.cs | 7 +- .../DeviceTypeInterfaces/ISelectableItem.cs | 40 +- .../DeviceTypeInterfaces/ISelectableItems.cs | 63 +- .../ISetTopBoxControls.cs | 7 +- .../ITemperatureSensor.cs | 43 +- .../DeviceTypeInterfaces/ITransport.cs | 175 +- .../ITvPresetsProvider.cs | 19 +- .../DeviceTypeInterfaces/IUiDisplayInfo.cs | 7 +- .../DeviceTypeInterfaces/IWarmingCooling.cs | 7 +- .../DeviceTypeInterfaces/LanguageLabel.cs | 46 +- .../DeviceTypeInterfaces/Template.cs | 7 +- .../Devices/AudioControlListItemBase.cs | 59 +- .../Devices/AudioInterfaces.cs | 7 +- .../Devices/CameraListItem.cs | 142 +- .../Devices/CodecInterfaces.cs | 183 +- .../Devices/CrestronProcessor.cs | 71 +- .../Devices/DestinationListItem.cs | 227 +- .../Devices/DeviceApiBase.cs | 27 +- .../Devices/DeviceFeedbackExtensions.cs | 37 +- .../Devices/DeviceJsonApi.cs | 958 ++--- .../Devices/DisplayUiConstants.cs | 7 +- .../Devices/EssentialsBridgeableDevice.cs | 34 +- .../Devices/EssentialsDevice.cs | 364 +- .../Devices/EssentialsDeviceFactory.cs | 42 +- .../Devices/GenericIRController.cs | 188 +- .../Devices/GenericMonitoredTcpDevice.cs | 9 +- .../Devices/IAttachVideoStatusExtensions.cs | 59 +- .../Devices/IHasFeedbacks.cs | 54 +- .../Devices/IProjectorInterfaces.cs | 57 +- .../Devices/IReconfigurableDevice.cs | 38 +- .../Devices/IUsageTracking.cs | 192 +- .../Devices/IrOutputPortController.cs | 353 +- .../Devices/LevelControlListItem.cs | 206 +- .../Devices/PduInterfaces.cs | 47 +- .../Devices/PowerInterfaces.cs | 247 +- .../Devices/PresentationDeviceType.cs | 13 +- .../Devices/PresetListItem.cs | 63 +- .../Devices/ReconfigurableDevice.cs | 134 +- .../Devices/SmartObjectBaseTypes.cs | 37 +- .../Devices/SourceListItem.cs | 540 ++- .../Devices/VolumeDeviceChangeEventArgs.cs | 10 +- .../eSourceListItemDestinationTypes.cs | 94 + .../Ethernet/EthernetStatistics.cs | 79 +- .../Extensions/IpAddressExtensions.cs | 131 +- .../Extensions/JsonExtensions.cs | 64 +- .../Extensions/StringExtensions.cs | 125 +- .../Factory/IDeviceFactory.cs | 39 +- .../IProcessorExtensionDeviceFactory.cs | 19 +- .../ProcessorExtensionDeviceFactory.cs | 6 +- .../Factory/ReadyEventArgs.cs | 62 +- .../Feedbacks/BoolFeedback.cs | 330 +- .../Feedbacks/BoolFeedbackOneShot.cs | 39 +- .../Feedbacks/BoolFeedbackPulseExtender.cs | 7 +- .../Feedbacks/BoolOutputLogicals.cs | 9 +- .../Feedbacks/FeedbackBase.cs | 7 +- .../Feedbacks/FeedbackCollection.cs | 21 +- .../Feedbacks/FeedbackEventArgs.cs | 159 +- .../Feedbacks/IntFeedback.cs | 200 +- .../Feedbacks/SerialFeedback.cs | 157 +- .../Feedbacks/StringFeedback.cs | 194 +- src/PepperDash.Essentials.Core/File/FileIO.cs | 133 +- ...entialsHuddleSpaceRoomFusionRoomJoinMap.cs | 434 +- .../Fusion/FusionCustomPropertiesBridge.cs | 186 +- .../Fusion/FusionEventHandlers.cs | 5 +- .../Fusion/FusionProcessorQueries.cs | 98 +- .../Fusion/FusionRviDataClasses.cs | 1170 ++---- .../Global/EthernetAdapterInfo.cs | 100 +- .../Global/Global.cs | 344 +- .../Global/JobTimer.cs | 9 +- .../Global/Scheduler.cs | 550 ++- .../InUseTracking/IInUseTracking.cs | 7 +- .../InUseTracking/InUseTracking.cs | 7 +- .../Interfaces/ILogStrings.cs | 21 +- .../Interfaces/ILogStringsWithLevel.cs | 22 +- .../JoinMaps/JoinMapBase.cs | 988 ++--- .../License/EssentialsLicenseManager.cs | 175 +- .../Lighting/Lighting Interfaces.cs | 188 +- .../MicrophonePrivacyController.cs | 429 +- .../MicrophonePrivacyControllerConfig.cs | 8 +- ...CrestronGenericBaseCommunicationMonitor.cs | 7 +- .../Monitoring/GenericCommunicationMonitor.cs | 616 ++- .../Monitoring/StatusMonitorBase.cs | 36 +- .../Monitoring/StatusMonitorCollection.cs | 7 +- .../Monitoring/SystemMonitorController.cs | 1952 +++++---- .../EssentialsPartitionController.cs | 358 +- .../IPartitionStateProvider.cs | 102 +- .../Plugins/IPluginDeviceFactory.cs | 32 +- .../Plugins/PluginLoader.cs | 21 +- .../Presets/DevicePresets.cs | 598 ++- .../Presets/DevicePresetsView.cs | 149 +- .../Presets/PresetChannel.cs | 77 +- .../PresetsListSubpageReferenceListItem.cs | 10 +- .../Queues/ComsMessage.cs | 116 +- .../Queues/GenericQueue.cs | 524 ++- .../Queues/IQueue.cs | 29 +- .../Queues/IQueueMessage.cs | 20 +- .../Queues/ProcessStringMessage.cs | 72 +- .../Queues/StringResponseProcessor.cs | 181 +- .../Ramps and Increments/ActionIncrementer.cs | 210 +- .../Ramps and Increments/NumericalHelpers.cs | 61 +- .../UshortSigIncrementer.cs | 186 +- .../RoomOnToDefaultSourceWhenOccupied.cs | 1008 +++-- .../Room/Combining/EssentialsRoomCombiner.cs | 844 ++-- .../EssentialsRoomCombinerPropertiesConfig.cs | 59 +- ...sentialsDualDisplayRoomPropertiesConfig.cs | 15 +- .../EssentialsHuddleRoomPropertiesConfig.cs | 49 +- .../EssentialsHuddleVtc1PropertiesConfig.cs | 23 +- .../EssentialsNDisplayRoomPropertiesConfig.cs | 90 +- .../Room/Config/EssentialsRoomConfig.cs | 843 ++-- .../Config/EssentialsRoomEmergencyConfig.cs | 53 +- .../EssentialsRoomScheduledEventsConfig.cs | 112 +- .../Room/Config/EssentialsTechRoomConfig.cs | 156 +- .../Config/EssentialsVolumeLevelConfig.cs | 128 +- .../Room/Config/SimplRoomPropertiesConfig.cs | 89 +- .../EsentialsRoomEmergencyContactClosure.cs | 162 +- .../Room/EssentialsRoomBase.cs | 864 ++-- .../Room/IEssentialsRoom.cs | 158 +- .../Room/IRoomEventSchedule.cs | 59 +- .../Room/Interfaces.cs | 489 +-- src/PepperDash.Essentials.Core/Room/Room.cs | 19 +- .../Room/iOccupancyStatusProvider.cs | 20 +- .../Routing/DummyRoutingInputsDevice.cs | 57 +- .../Routing/Extensions.cs | 787 ++-- .../Routing/IHasCurrentSourceInfoChange.cs | 13 +- .../Routing/IInputSync.cs | 4 - .../Routing/IRmcRouting.cs | 20 +- .../Routing/IRmcRoutingWithFeedback.cs | 13 +- .../Routing/IRouting.cs | 29 +- .../Routing/IRoutingFeedback.cs | 22 +- .../IRoutingHasVideoInputSyncFeedbacks.cs | 7 +- .../Routing/IRoutingInputSlot.cs | 19 +- .../Routing/IRoutingInputs.cs | 23 +- .../Routing/IRoutingInputsOutputs.cs | 23 +- .../Routing/IRoutingNumeric.cs | 26 +- .../Routing/IRoutingNumericWithFeedback.cs | 13 +- .../Routing/IRoutingOutputSlot.cs | 35 +- .../Routing/IRoutingOutputs.cs | 24 +- .../Routing/IRoutingSink.cs | 46 +- .../Routing/IRoutingSinkWithFeedback.cs | 29 +- .../Routing/IRoutingSinkWithSwitching.cs | 59 +- .../Routing/IRoutingSlot.cs | 5 - .../Routing/IRoutingSource.cs | 13 +- .../Routing/IRoutingWithClear.cs | 21 +- .../Routing/IRoutingWithFeedback.cs | 45 +- .../Routing/ITxRouting.cs | 26 +- .../Routing/ITxRoutingWithFeedback.cs | 13 +- .../Routing/RouteDescriptor.cs | 358 +- .../Routing/RouteDescriptorCollection.cs | 168 +- .../Routing/RouteRequest.cs | 123 +- .../Routing/RouteRequestQueueItem.cs | 145 +- .../Routing/RouteSwitchDescriptor.cs | 65 +- .../Routing/RoutingFeedbackManager.cs | 594 ++- .../Routing/RoutingInputPort.cs | 72 +- .../RoutingInputPortWithVideoStatuses.cs | 54 +- .../Routing/RoutingNumericEventArgs.cs | 137 +- .../Routing/RoutingOutputPort.cs | 144 +- .../Routing/RoutingPort.cs | 68 +- .../Routing/RoutingPortCollection.cs | 22 +- .../Routing/RoutingPortNames.cs | 423 +- .../Routing/TieLine.cs | 270 +- .../Routing/TieLineConfig.cs | 7 +- .../Routing/eRoutingPortConnectionType.cs | 2 +- .../Routing/eRoutingSignalType.cs | 2 +- .../Secrets/CrestronGlobalSecretsProvider.cs | 190 +- .../Secrets/CrestronLocalSecretsProvider.cs | 192 +- .../Secrets/CrestronSecret.cs | 48 +- .../Secrets/Interfaces.cs | 93 +- .../Secrets/SecretsManager.cs | 692 ++-- .../Secrets/SecretsPropertiesConfig.cs | 28 +- .../Shades/Shade Interfaces.cs | 303 +- .../Shades/ShadeBase.cs | 66 +- src/PepperDash.Essentials.Core/SigHelper.cs | 7 +- .../SmartObjects/SmartObjectDPad.cs | 71 +- .../SmartObjects/SmartObjectDynamicList.cs | 261 +- .../SmartObjects/SmartObjectHelperBase.cs | 163 +- .../SmartObjects/SmartObjectNumeric.cs | 170 +- .../SubpageReferenceList.cs | 515 ++- .../SubpageReferenceListItem.cs | 81 +- .../Timers/CountdownTimer.cs | 290 +- .../Timers/RetriggerableTimer.cs | 312 +- .../CrestronTouchpanelPropertiesConfig.cs | 195 +- .../Touchpanels/Interfaces.cs | 27 +- .../Keyboards/HabaneroKeyboardController.cs | 790 ++-- .../Touchpanels/ModalDialog.cs | 120 +- .../Touchpanels/Mpc3Touchpanel.cs | 85 +- .../Touchpanels/TriListExtensions.cs | 119 +- .../TriListBridges/HandlerBridge.cs | 42 +- .../UI PageManagers/BlurayPageManager.cs | 11 +- .../UI PageManagers/PageManager.cs | 7 +- .../SetTopBoxThreePanelPageManager.cs | 405 +- .../SetTopBoxTwoPanelPageManager.cs | 7 +- .../UI PageManagers/SinglePageManager.cs | 7 +- .../UI/TouchpanelBase.cs | 268 +- .../Utilities/ActionSequence.cs | 250 +- .../VideoStatus/VideoStatusOutputs.cs | 7 +- .../Web/EssentialsWebApi.cs | 505 ++- .../Web/EssentialsWebApiFactory.cs | 47 +- .../Web/EssentialsWebApiHelpers.cs | 184 +- .../Web/EssentialsWebApiPropertiesConfig.cs | 19 +- .../RequestHandlers/AppDebugRequestHandler.cs | 156 +- .../DebugSessionRequestHandler.cs | 165 +- .../Web/RequestHandlers/DebugWebsocketSink.cs | 63 +- .../RequestHandlers/DefaultRequestHandler.cs | 10 +- .../RequestHandlers/DevJsonRequestHandler.cs | 48 +- .../RequestHandlers/DevListRequestHandler.cs | 80 +- .../DevMethodsRequestHandler.cs | 133 +- .../RequestHandlers/DevPropsRequestHandler.cs | 131 +- .../DisableAllStreamDebugRequestHandler.cs | 48 +- ...DoNotLoadConfigOnNextBootRequestHandler.cs | 150 +- .../GetFeedbacksForDeviceRequestHandler.cs | 172 +- .../GetJoinMapForBridgeKeyRequestHandler.cs | 128 +- .../GetJoinMapForDeviceKeyRequestHandler.cs | 168 +- .../Web/RequestHandlers/GetRoutesHandler.cs | 98 +- .../RequestHandlers/GetRoutingPortsHandler.cs | 125 +- .../GetTieLinesRequestHandler.cs | 55 +- .../GetTypesByFilterRequestHandler.cs | 117 +- .../RequestHandlers/GetTypesRequestHandler.cs | 97 +- .../LoadConfigRequestHandler.cs | 68 +- .../ReportVersionsRequestHandler.cs | 79 +- .../RestartProgramRequestHandler.cs | 64 +- .../SetDeviceStreamDebugRequestHandler.cs | 420 +- .../ShowConfigRequestHandler.cs | 59 +- .../Audio/GenericAudioOut.cs | 240 +- .../AudioCodec/AudioCodecBase.cs | 189 +- .../AudioCodec/Interfaces/IAudioCodecInfo.cs | 38 +- .../AudioCodec/Interfaces/IHasAudioCodec.cs | 19 +- .../AudioCodec/MockAC/MockAC.cs | 240 +- .../MockAC/MockAcPropertiesConfig.cs | 17 +- .../Cameras/CameraBase.cs | 557 ++- .../Cameras/CameraVisca.cs | 1331 +++---- .../Cameras/IHasCameraPresets.cs | 50 +- .../Codec/Cisco/IPresenterTrack.cs | 139 +- .../Codec/Cisco/ISpeakerTrack.cs | 49 +- .../Codec/CodecActiveCallItem.cs | 175 +- .../Codec/IHasCallHold.cs | 30 +- .../Codec/IHasDoNotDisturb.cs | 67 +- .../Codec/IHasExternalSourceSwitching.cs | 82 +- .../Codec/eCodecCallDirection.cs | 92 +- .../Codec/eCodecCallStatus.cs | 255 +- .../Codec/eCodecCallType.cs | 125 +- .../Codec/eMeetingPrivacy.cs | 89 +- .../Codec/iCodecAudio.cs | 15 +- .../Codec/iHasCallFavorites.cs | 48 +- .../Codec/iHasCallHistory.cs | 259 +- .../Codec/iHasContentSharing.cs | 52 +- .../Codec/iHasDialer.cs | 89 +- .../DSP/DspBase.cs | 260 +- .../Displays/BasicIrDisplay.cs | 526 +-- .../Displays/DisplayBase.cs | 721 ++-- .../Displays/InputInterfaces.cs | 130 +- .../Displays/MockDisplay.cs | 591 ++- .../Displays/MockDisplayInputs.cs | 184 +- .../Displays/ScreenLiftController.cs | 4 +- .../Generic/GenericSink.cs | 224 +- .../Generic/GenericSource.cs | 127 +- .../Lighting/LightingBase.cs | 266 +- .../Room/IEssentialsHuddleSpaceRoom.cs | 39 +- .../Room/IEssentialsHuddleVtc1Room.cs | 76 +- .../Room/IEssentialsRoomPropertiesConfig.cs | 19 +- .../Room/IEssentialsTechRoom.cs | 49 +- .../SetTopBox/IRSetTopBoxBase.cs | 2 +- .../SetTopBox/SetTopBoxPropertiesConfig.cs | 59 +- .../Shades/RelayControlledShade.cs | 306 +- .../Shades/ShadeBase.cs | 59 +- .../Shades/ShadeController.cs | 161 +- .../SoftCodec/BlueJeansPc.cs | 331 +- .../SoftCodec/GenericSoftCodec.cs | 238 +- .../Sources/InRoomPc.cs | 121 +- .../Sources/Laptop.cs | 131 +- .../Streaming/AppleTV.cs | 462 +-- .../CiscoCodec/CallHistoryDataClasses.cs | 420 +- .../VideoCodec/CiscoCodec/RoomPresets.cs | 137 +- .../VideoCodec/CiscoCodec/eCommandType.cs | 7 +- .../CiscoCodec/eExternalSourceMode.cs | 2 +- .../CiscoCodec/eExternalSourceType.cs | 2 +- .../VideoCodec/ConvertiblePreset.cs | 22 +- .../VideoCodec/Interfaces/IHasCodecLayouts.cs | 43 +- .../Interfaces/IHasCodecSelfview.cs | 52 +- .../VideoCodec/Interfaces/IHasMeetingInfo.cs | 28 +- .../VideoCodec/Interfaces/IHasMeetingLock.cs | 43 +- .../Interfaces/IHasMeetingRecording.cs | 44 +- .../VideoCodec/Interfaces/IHasParticipants.cs | 2 +- .../Interfaces/IHasPresentationOnlyMeeting.cs | 103 +- .../Interfaces/IHasSelfviewPosition.cs | 36 +- .../VideoCodec/Interfaces/IHasSelfviewSize.cs | 35 +- .../VideoCodec/Interfaces/IHasStandbyMode.cs | 72 +- .../VideoCodec/Interfaces/IHasStartMeeting.cs | 35 +- .../VideoCodec/Interfaces/IHasVideoCodec.cs | 33 +- .../VideoCodec/Interfaces/IJoinCalls.cs | 30 +- .../VideoCodec/MockVC/MockVCCamera.cs | 433 +- .../MockVC/MockVcPropertiesConfig.cs | 48 +- .../VideoCodec/VideoCodecBase.cs | 3537 ++++++++--------- .../ContentTypes.cs | 14 +- .../DisplayBaseMessenger.cs | 9 +- .../DeviceTypeExtensions/IChannelMessenger.cs | 7 + .../DeviceTypeExtensions/IColorMessenger.cs | 8 + .../DeviceTypeExtensions/IDPadMessenger.cs | 9 +- .../DeviceTypeExtensions/IDvrMessenger.cs | 12 +- .../IHasPowerMessenger.cs | 9 + .../DeviceTypeExtensions/INumericMessenger.cs | 9 + .../ISetTopBoxControlsMessenger.cs | 15 +- .../ITransportMessenger.cs | 11 +- .../Messengers/DeviceInfoMessenger.cs | 5 +- .../Messengers/DeviceVolumeMessenger.cs | 21 +- .../Messengers/GenericMessenger.cs | 2 +- .../ICommunicationMonitorMessenger.cs | 2 +- .../Messengers/IDspPresetsMessenger.cs | 14 +- .../IEssentialsRoomCombinerMessenger.cs | 2 +- .../IHasCurrentSourceInfoMessenger.cs | 2 +- .../Messengers/IHasInputsMessenger.cs | 12 +- .../IHasPowerControlWithFeedbackMessenger.cs | 2 +- .../IHasScheduleAwarenessMessenger.cs | 156 +- .../Messengers/IHumiditySensor.cs | 9 +- .../Messengers/ILevelControlsMessenger.cs | 17 +- .../Messengers/IMatrixRoutingMessenger.cs | 84 +- .../IProjectorScreenLiftControlMessenger.cs | 2 +- .../Messengers/ISelectableItemsMessenger.cs | 2 +- .../IShutdownPromptTimerMessenger.cs | 9 +- .../Messengers/ISwitchedOutputMessenger.cs | 10 +- .../Messengers/ITechPasswordMessenger.cs | 19 +- .../Messengers/ITemperatureSensorMessenger.cs | 10 +- .../Messengers/LightingBaseMessenger.cs | 8 + .../Messengers/MessengerBase.cs | 2 +- .../Messengers/PressAndHoldHandler.cs | 2 +- .../Messengers/RoomEventScheduleMessenger.cs | 167 +- .../Messengers/ShadeBaseMessenger.cs | 14 + .../Messengers/SystemMonitorMessenger.cs | 1 + .../Messengers/TwoWayDisplayBaseMessenger.cs | 7 + .../MobileControlMessage.cs | 8 +- .../MobileControlSimpleContent.cs | 22 +- .../SIMPLJoinMaps/SIMPLVtcJoinMap.cs | 3 +- .../AuthorizationResponse.cs | 59 +- .../MobileControlAction.cs | 47 +- .../MobileControlDeviceFactory.cs | 44 +- .../MobileControlEssentialsConfig.cs | 119 +- .../MobileControlSystemController.cs | 2 +- .../RoomBridges/MobileControlBridgeBase.cs | 293 +- .../Services/MobileControlApiService.cs | 121 +- .../Touchpanel/ITheme.cs | 28 +- .../Touchpanel/ITswAppControl.cs | 81 +- .../Touchpanel/ITswAppControlMessenger.cs | 2 +- .../Touchpanel/ITswZoomControlMessenger.cs | 2 +- .../MobileControlTouchpanelController.cs | 2 +- .../MobileControlTouchpanelProperties.cs | 57 +- .../Touchpanel/ThemeMessenger.cs | 2 +- .../UserCodeChangedContent.cs | 32 +- .../Volumes.cs | 248 +- .../WebApiHandlers/ActionPathsHandler.cs | 2 +- .../MobileAuthRequestHandler.cs | 2 +- .../WebApiHandlers/MobileInfoHandler.cs | 2 +- .../WebApiHandlers/UiClientHandler.cs | 1 + .../MobileControlWebsocketServer.cs | 64 +- .../WebSocketServer/UiClient.cs | 2 +- .../WebSocketServerSecretProvider.cs | 2 +- ...lsHuddleSpaceFusionSystemControllerBase.cs | 53 +- src/PepperDash.Essentials/HttpLogoServer.cs | 178 +- 522 files changed, 39628 insertions(+), 45678 deletions(-) delete mode 100644 src/PepperDash.Core/ComTextHelper.cs delete mode 100644 src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/eSourceListItemDestinationTypes.cs diff --git a/src/PepperDash.Core/ComTextHelper.cs b/src/PepperDash.Core/ComTextHelper.cs deleted file mode 100644 index 28a76975..00000000 --- a/src/PepperDash.Core/ComTextHelper.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace PepperDash.Core -{ - /// - /// Helper class for formatting communication text and byte data for debugging purposes. - /// - 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/CommunicationGather.cs b/src/PepperDash.Core/Comm/CommunicationGather.cs index e75a012d..4131d364 100644 --- a/src/PepperDash.Core/Comm/CommunicationGather.cs +++ b/src/PepperDash.Core/Comm/CommunicationGather.cs @@ -8,8 +8,8 @@ using Crestron.SimplSharp; using PepperDash.Core; -namespace PepperDash.Core -{ +namespace PepperDash.Core; + /// /// Defines the string event handler for line events on the gather /// @@ -30,7 +30,7 @@ namespace PepperDash.Core /// /// The communication port that this gathers on /// - public ICommunicationReceiver Port { get; private set; } + public ICommunicationReceiver Port { get; private set; } /// /// Default false. If true, the delimiter will be included in the line output @@ -67,22 +67,22 @@ namespace PepperDash.Core /// /// /// - public CommunicationGather(ICommunicationReceiver port, string delimiter) - :this(port, new string[] { delimiter} ) + public CommunicationGather(ICommunicationReceiver port, string delimiter) + :this(port, new string[] { delimiter} ) { } - /// - /// Constructor for using an array of string delimiters - /// - /// - /// - public CommunicationGather(ICommunicationReceiver port, string[] delimiters) - { - Port = port; - StringDelimiters = delimiters; - port.TextReceived += Port_TextReceivedStringDelimiter; - } + /// + /// Constructor for using an array of string delimiters + /// + /// + /// + public CommunicationGather(ICommunicationReceiver port, string[] delimiters) + { + Port = port; + StringDelimiters = delimiters; + port.TextReceived += Port_TextReceivedStringDelimiter; + } /// /// Stop method @@ -135,35 +135,35 @@ namespace PepperDash.Core ReceiveBuffer.Append(args.Text); var str = ReceiveBuffer.ToString(); - // Case: Receiving DEVICE get version\x0d\0x0a+OK "value":"1234"\x0d\x0a + // Case: Receiving DEVICE get version\x0d\0x0a+OK "value":"1234"\x0d\x0a - // RX: DEV - // Split: (1) "DEV" - // RX: I - // Split: (1) "DEVI" - // RX: CE get version - // Split: (1) "DEVICE get version" - // RX: \x0d\x0a+OK "value":"1234"\x0d\x0a - // Split: (2) DEVICE get version, +OK "value":"1234" + // RX: DEV + // Split: (1) "DEV" + // RX: I + // Split: (1) "DEVI" + // RX: CE get version + // Split: (1) "DEVICE get version" + // RX: \x0d\x0a+OK "value":"1234"\x0d\x0a + // Split: (2) DEVICE get version, +OK "value":"1234" - // Iterate the delimiters and fire an event for any matching delimiter - foreach (var delimiter in StringDelimiters) + // Iterate the delimiters and fire an event for any matching delimiter + foreach (var delimiter in StringDelimiters) + { + var lines = Regex.Split(str, delimiter); + if (lines.Length == 1) + continue; + + for (int i = 0; i < lines.Length - 1; i++) { - var lines = Regex.Split(str, delimiter); - if (lines.Length == 1) - continue; - - for (int i = 0; i < lines.Length - 1; i++) - { - string strToSend = null; - if (IncludeDelimiter) - strToSend = lines[i] + delimiter; - else - strToSend = lines[i]; - handler(this, new GenericCommMethodReceiveTextArgs(strToSend, delimiter)); - } - ReceiveBuffer = new StringBuilder(lines[lines.Length - 1]); + string strToSend = null; + if (IncludeDelimiter) + strToSend = lines[i] + delimiter; + else + strToSend = lines[i]; + handler(this, new GenericCommMethodReceiveTextArgs(strToSend, delimiter)); } + ReceiveBuffer = new StringBuilder(lines[lines.Length - 1]); + } } } @@ -174,5 +174,4 @@ namespace PepperDash.Core { Stop(); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs index 780e65d0..4739912a 100644 --- a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs +++ b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs @@ -1,138 +1,157 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text; using Crestron.SimplSharp; using PepperDash.Core; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Controls the ability to disable/enable debugging of TX/RX data sent to/from a device with a built in timer to disable +/// +public class CommunicationStreamDebugging { /// - /// Controls the ability to disable/enable debugging of TX/RX data sent to/from a device with a built in timer to disable + /// Device Key that this instance configures /// - public class CommunicationStreamDebugging + public string ParentDeviceKey { get; private set; } + + /// + /// Timer to disable automatically if not manually disabled + /// + private CTimer DebugExpiryPeriod; + + /// + /// The current debug setting + /// + public eStreamDebuggingSetting DebugSetting { get; private set; } + + private uint _DebugTimeoutInMs; + private const uint _DefaultDebugTimeoutMin = 30; + + /// + /// Timeout in Minutes + /// + public uint DebugTimeoutMinutes { - /// - /// Device Key that this instance configures - /// - public string ParentDeviceKey { get; private set; } - - /// - /// Timer to disable automatically if not manually disabled - /// - private CTimer DebugExpiryPeriod; - - /// - /// Gets or sets the DebugSetting - /// - public eStreamDebuggingSetting DebugSetting { get; private set; } - - private uint _DebugTimeoutInMs; - private const uint _DefaultDebugTimeoutMin = 30; - - /// - /// Timeout in Minutes - /// - public uint DebugTimeoutMinutes + get { - get - { - return _DebugTimeoutInMs / 60000; - } - } - - /// - /// Gets or sets the RxStreamDebuggingIsEnabled - /// - public bool RxStreamDebuggingIsEnabled { get; private set; } - - /// - /// Indicates that transmit stream debugging is enabled - /// - public bool TxStreamDebuggingIsEnabled { get; private set; } - - /// - /// Constructor - /// - /// - public CommunicationStreamDebugging(string parentDeviceKey) - { - ParentDeviceKey = parentDeviceKey; - } - - - /// - /// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues - /// - /// - /// - /// SetDebuggingWithDefaultTimeout method - /// - public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting) - { - if (setting == eStreamDebuggingSetting.Off) - { - DisableDebugging(); - return; - } - - SetDebuggingWithSpecificTimeout(setting, _DefaultDebugTimeoutMin); - } - - /// - /// Sets the debugging setting for the specified number of minutes - /// - /// - /// - /// - /// SetDebuggingWithSpecificTimeout method - /// - public void SetDebuggingWithSpecificTimeout(eStreamDebuggingSetting setting, uint minutes) - { - if (setting == eStreamDebuggingSetting.Off) - { - DisableDebugging(); - return; - } - - _DebugTimeoutInMs = minutes * 60000; - - StopDebugTimer(); - - DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs); - - if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx) - RxStreamDebuggingIsEnabled = true; - - if ((setting & eStreamDebuggingSetting.Tx) == eStreamDebuggingSetting.Tx) - TxStreamDebuggingIsEnabled = true; - - Debug.SetDeviceDebugSettings(ParentDeviceKey, setting); - - } - - /// - /// Disabled debugging - /// - private void DisableDebugging() - { - StopDebugTimer(); - - Debug.SetDeviceDebugSettings(ParentDeviceKey, eStreamDebuggingSetting.Off); - } - - private void StopDebugTimer() - { - RxStreamDebuggingIsEnabled = false; - TxStreamDebuggingIsEnabled = false; - - if (DebugExpiryPeriod == null) - { - return; - } - - DebugExpiryPeriod.Stop(); - DebugExpiryPeriod.Dispose(); - DebugExpiryPeriod = null; + return _DebugTimeoutInMs/60000; } } + + /// + /// Indicates that receive stream debugging is enabled + /// + public bool RxStreamDebuggingIsEnabled{ get; private set; } + + /// + /// Indicates that transmit stream debugging is enabled + /// + public bool TxStreamDebuggingIsEnabled { get; private set; } + + /// + /// Constructor + /// + /// + public CommunicationStreamDebugging(string parentDeviceKey) + { + ParentDeviceKey = parentDeviceKey; + } + + + /// + /// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues + /// + /// + public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting) + { + if (setting == eStreamDebuggingSetting.Off) + { + DisableDebugging(); + return; + } + + SetDebuggingWithSpecificTimeout(setting, _DefaultDebugTimeoutMin); + } + + /// + /// Sets the debugging setting for the specified number of minutes + /// + /// + /// + public void SetDebuggingWithSpecificTimeout(eStreamDebuggingSetting setting, uint minutes) + { + if (setting == eStreamDebuggingSetting.Off) + { + DisableDebugging(); + return; + } + + _DebugTimeoutInMs = minutes * 60000; + + StopDebugTimer(); + + DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs); + + if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx) + RxStreamDebuggingIsEnabled = true; + + if ((setting & eStreamDebuggingSetting.Tx) == eStreamDebuggingSetting.Tx) + TxStreamDebuggingIsEnabled = true; + + Debug.SetDeviceDebugSettings(ParentDeviceKey, setting); + + } + + /// + /// Disabled debugging + /// + private void DisableDebugging() + { + StopDebugTimer(); + + Debug.SetDeviceDebugSettings(ParentDeviceKey, eStreamDebuggingSetting.Off); + } + + private void StopDebugTimer() + { + RxStreamDebuggingIsEnabled = false; + TxStreamDebuggingIsEnabled = false; + + if (DebugExpiryPeriod == null) + { + return; + } + + DebugExpiryPeriod.Stop(); + DebugExpiryPeriod.Dispose(); + DebugExpiryPeriod = null; + } } + +/// +/// 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/Comm/ControlPropertiesConfig.cs b/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs index a7d2300a..e359eade 100644 --- a/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs +++ b/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs @@ -8,91 +8,90 @@ using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using NullValueHandling = NewtonsoftJson::Newtonsoft.Json.NullValueHandling; using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Config properties that indicate how to communicate with a device for control +/// +public class ControlPropertiesConfig { /// - /// Represents a ControlPropertiesConfig + /// The method of control /// - public class ControlPropertiesConfig - { - /// - /// The method of control - /// - [JsonProperty("method")] - [JsonConverter(typeof(StringEnumConverter))] - public eControlMethod Method { get; set; } + [JsonProperty("method")] + [JsonConverter(typeof(StringEnumConverter))] + public eControlMethod Method { get; set; } - /// - /// The key of the device that contains the control port - /// - [JsonProperty("controlPortDevKey", NullValueHandling = NullValueHandling.Ignore)] - public string ControlPortDevKey { get; set; } + /// + /// The key of the device that contains the control port + /// + [JsonProperty("controlPortDevKey", NullValueHandling = NullValueHandling.Ignore)] + public string ControlPortDevKey { get; set; } - /// - /// The number of the control port on the device specified by ControlPortDevKey - /// - [JsonProperty("controlPortNumber", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value - public uint? ControlPortNumber { get; set; } + /// + /// The number of the control port on the device specified by ControlPortDevKey + /// + [JsonProperty("controlPortNumber", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value + public uint? ControlPortNumber { get; set; } - /// - /// The name of the control port on the device specified by ControlPortDevKey - /// - [JsonProperty("controlPortName", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value - public string ControlPortName { get; set; } + /// + /// The name of the control port on the device specified by ControlPortDevKey + /// + [JsonProperty("controlPortName", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value + public string ControlPortName { get; set; } - /// - /// Properties for ethernet based communications - /// - [JsonProperty("tcpSshProperties", NullValueHandling = NullValueHandling.Ignore)] - public TcpSshPropertiesConfig TcpSshProperties { get; set; } + /// + /// Properties for ethernet based communications + /// + [JsonProperty("tcpSshProperties", NullValueHandling = NullValueHandling.Ignore)] + public TcpSshPropertiesConfig TcpSshProperties { get; set; } - /// - /// The filename and path for the IR file - /// - [JsonProperty("irFile", NullValueHandling = NullValueHandling.Ignore)] - public string IrFile { get; set; } + /// + /// The filename and path for the IR file + /// + [JsonProperty("irFile", NullValueHandling = NullValueHandling.Ignore)] + public string IrFile { get; set; } - /// - /// The IpId of a Crestron device - /// - [JsonProperty("ipId", NullValueHandling = NullValueHandling.Ignore)] - public string IpId { get; set; } + /// + /// The IpId of a Crestron device + /// + [JsonProperty("ipId", NullValueHandling = NullValueHandling.Ignore)] + public string IpId { get; set; } - /// - /// Readonly uint representation of the IpId - /// - [JsonIgnore] - public uint IpIdInt { get { return Convert.ToUInt32(IpId, 16); } } + /// + /// Readonly uint representation of the IpId + /// + [JsonIgnore] + public uint IpIdInt { get { return Convert.ToUInt32(IpId, 16); } } - /// - /// Char indicating end of line - /// - [JsonProperty("endOfLineChar", NullValueHandling = NullValueHandling.Ignore)] - public char EndOfLineChar { get; set; } + /// + /// Char indicating end of line + /// + [JsonProperty("endOfLineChar", NullValueHandling = NullValueHandling.Ignore)] + public char EndOfLineChar { get; set; } - /// - /// Defaults to Environment.NewLine; - /// - [JsonProperty("endOfLineString", NullValueHandling = NullValueHandling.Ignore)] - public string EndOfLineString { get; set; } + /// + /// Defaults to Environment.NewLine; + /// + [JsonProperty("endOfLineString", NullValueHandling = NullValueHandling.Ignore)] + public string EndOfLineString { get; set; } - /// - /// Indicates - /// - [JsonProperty("deviceReadyResponsePattern", NullValueHandling = NullValueHandling.Ignore)] - public string DeviceReadyResponsePattern { get; set; } + /// + /// Indicates + /// + [JsonProperty("deviceReadyResponsePattern", NullValueHandling = NullValueHandling.Ignore)] + public string DeviceReadyResponsePattern { get; set; } - /// - /// Used when communcating to programs running in VC-4 - /// - [JsonProperty("roomId", NullValueHandling = NullValueHandling.Ignore)] - public string RoomId { get; set; } + /// + /// Used when communcating to programs running in VC-4 + /// + [JsonProperty("roomId", NullValueHandling = NullValueHandling.Ignore)] + public string RoomId { get; set; } - /// - /// Constructor - /// - public ControlPropertiesConfig() - { - } + /// + /// Constructor + /// + public ControlPropertiesConfig() + { } } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/EventArgs.cs b/src/PepperDash.Core/Comm/EventArgs.cs index 0b394423..d68dda7c 100644 --- a/src/PepperDash.Core/Comm/EventArgs.cs +++ b/src/PepperDash.Core/Comm/EventArgs.cs @@ -16,236 +16,237 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Delegate for notifying of socket status changes +/// +/// +public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client); + +/// +/// EventArgs class for socket status changes +/// +public class GenericSocketStatusChageEventArgs : EventArgs { /// - /// Delegate for notifying of socket status changes + /// Gets or sets the Client + /// + public ISocketStatus Client { get; private set; } + + /// + /// Constructor /// /// - public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client); - - /// - /// EventArgs class for socket status changes - /// - public class GenericSocketStatusChageEventArgs : EventArgs - { - /// - /// Gets or sets the Client - /// - public ISocketStatus Client { get; private set; } - - /// - /// - /// - /// - public GenericSocketStatusChageEventArgs(ISocketStatus client) - { - Client = client; - } - /// - /// S+ Constructor - /// - public GenericSocketStatusChageEventArgs() { } + public GenericSocketStatusChageEventArgs(ISocketStatus client) + { + Client = client; } + /// + /// S+ Constructor + /// + public GenericSocketStatusChageEventArgs() { } +} + +/// +/// Delegate for notifying of TCP Server state changes +/// +/// +public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state); + +/// +/// EventArgs class for TCP Server state changes +/// +public class GenericTcpServerStateChangedEventArgs : EventArgs +{ + /// + /// Gets or sets the State + /// + public ServerState State { get; private set; } /// - /// Delegate for notifying of TCP Server state changes + /// Constructor /// /// - public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state); + public GenericTcpServerStateChangedEventArgs(ServerState state) + { + State = state; + } + /// + /// S+ Constructor + /// + public GenericTcpServerStateChangedEventArgs() { } +} + +/// +/// Delegate for TCP Server socket status changes +/// +/// +/// +/// +public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus); +/// +/// EventArgs for TCP server socket status changes +/// +public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs +{ + /// + /// Gets or sets the Socket + /// + public object Socket { get; private set; } /// - /// EventArgs class for TCP Server state changes + /// Gets or sets the index of the client from which the status change was received /// - public class GenericTcpServerStateChangedEventArgs : EventArgs - { - /// - /// Gets or sets the State - /// - public ServerState State { get; private set; } + public uint ReceivedFromClientIndex { get; private set; } - /// - /// - /// - /// - public GenericTcpServerStateChangedEventArgs(ServerState state) - { - State = state; - } - /// - /// S+ Constructor - /// - public GenericTcpServerStateChangedEventArgs() { } + /// + /// Gets or sets the ClientStatus + /// + public SocketStatus ClientStatus { get; set; } + + /// + /// Constructor + /// + /// + /// + public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus) + { + Socket = socket; + ClientStatus = clientStatus; } /// - /// Delegate for TCP Server socket status changes + /// Constructor /// /// /// /// - public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus); - /// - /// EventArgs for TCP server socket status changes - /// - public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs + public GenericTcpServerSocketStatusChangeEventArgs(object socket, uint clientIndex, SocketStatus clientStatus) { - /// - /// - /// - public object Socket { get; private set; } - /// - /// - /// - public uint ReceivedFromClientIndex { get; private set; } - /// - /// - /// - public SocketStatus ClientStatus { get; set; } + Socket = socket; + ReceivedFromClientIndex = clientIndex; + ClientStatus = clientStatus; + } + /// + /// S+ Constructor + /// + public GenericTcpServerSocketStatusChangeEventArgs() { } +} - /// - /// - /// - /// - /// - public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus) - { - Socket = socket; - ClientStatus = clientStatus; - } +/// +/// EventArgs for TCP server com method receive text +/// +public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs +{ + /// + /// Gets or sets the index of the client from which the text was received + /// + public uint ReceivedFromClientIndex { get; private set; } - /// - /// - /// - /// - /// - /// - public GenericTcpServerSocketStatusChangeEventArgs(object socket, uint clientIndex, SocketStatus clientStatus) + /// + /// Gets the index of the client from which the text was received as a ushort + /// + public ushort ReceivedFromClientIndexShort + { + get { - Socket = socket; - ReceivedFromClientIndex = clientIndex; - ClientStatus = clientStatus; + return (ushort)ReceivedFromClientIndex; } - /// - /// S+ Constructor - /// - public GenericTcpServerSocketStatusChangeEventArgs() { } } /// - /// EventArgs for TCP server com method receive text + /// Gets or sets the Text /// - public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs + public string Text { get; private set; } + + /// + /// Constructor + /// + /// + public GenericTcpServerCommMethodReceiveTextArgs(string text) { - /// - /// - /// - public uint ReceivedFromClientIndex { get; private set; } - - /// - /// - /// - public ushort ReceivedFromClientIndexShort - { - get - { - return (ushort)ReceivedFromClientIndex; - } - } - - /// - /// Gets or sets the Text - /// - public string Text { get; private set; } - - /// - /// - /// - /// - public GenericTcpServerCommMethodReceiveTextArgs(string text) - { - Text = text; - } - - /// - /// - /// - /// - /// - public GenericTcpServerCommMethodReceiveTextArgs(string text, uint clientIndex) - { - Text = text; - ReceivedFromClientIndex = clientIndex; - } - /// - /// S+ Constructor - /// - public GenericTcpServerCommMethodReceiveTextArgs() { } + Text = text; } /// - /// EventArgs for TCP server client ready for communication + /// Constructor /// - public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs + /// + /// + public GenericTcpServerCommMethodReceiveTextArgs(string text, uint clientIndex) { - /// - /// - /// - public bool IsReady; + Text = text; + ReceivedFromClientIndex = clientIndex; + } + /// + /// S+ Constructor + /// + public GenericTcpServerCommMethodReceiveTextArgs() { } +} - /// - /// - /// - /// - public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady) - { - IsReady = isReady; - } - /// - /// S+ Constructor - /// - public GenericTcpServerClientReadyForcommunicationsEventArgs() { } +/// +/// EventArgs for TCP server client ready for communication +/// +public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs +{ + /// + /// Gets or sets IsReady + /// + public bool IsReady; + + /// + /// Constructor + /// + /// + public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady) + { + IsReady = isReady; } /// - /// EventArgs for UDP connected + /// S+ Constructor /// - public class GenericUdpConnectedEventArgs : EventArgs + public GenericTcpServerClientReadyForcommunicationsEventArgs() { } +} + +/// +/// EventArgs for UDP connected +/// +public class GenericUdpConnectedEventArgs : EventArgs +{ + /// + /// Gets or sets the UConnected + /// + public ushort UConnected; + + /// + /// Gets or sets the Connected status + /// + public bool Connected; + + /// + /// Constructor + /// + public GenericUdpConnectedEventArgs() { } + + /// + /// Constructor + /// + /// + public GenericUdpConnectedEventArgs(ushort uconnected) { - /// - /// - /// - public ushort UConnected; - /// - /// - /// - public bool Connected; - - /// - /// Constructor - /// - public GenericUdpConnectedEventArgs() { } - - /// - /// - /// - /// - public GenericUdpConnectedEventArgs(ushort uconnected) - { - UConnected = uconnected; - } - - /// - /// - /// - /// - public GenericUdpConnectedEventArgs(bool connected) - { - Connected = connected; - } - + UConnected = uconnected; } - + /// + /// Constructor + /// + /// + public GenericUdpConnectedEventArgs(bool connected) + { + Connected = connected; + } + +} -} \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs index 204c548b..36e405b0 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs @@ -7,952 +7,947 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// A class to handle secure TCP/IP communications with a server +/// +public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect { + private const string SplusKey = "Uninitialized Secure Tcp _client"; /// - /// A class to handle secure TCP/IP communications with a server + /// Stream debugging /// - public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect + 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 text + /// + public event EventHandler TextReceived; + + #region GenericSecureTcpIpClient Events & Delegates + + /// + /// + /// + //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; + public event EventHandler ConnectionChange; + + /// + /// Auto reconnect evant handler + /// + public event EventHandler AutoReconnectTriggered; + + /// + /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. + /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. + /// + public event EventHandler TextReceivedQueueInvoke; + + /// + /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require + /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. + /// + public event EventHandler ClientReadyForCommunications; + + #endregion + + + #region GenricTcpIpClient properties + + private string _hostname; + + /// + /// Address of server + /// + public string Hostname { - private const string SplusKey = "Uninitialized Secure Tcp _client"; - /// - /// Stream debugging - /// - 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 text - /// - public event EventHandler TextReceived; - - #region GenericSecureTcpIpClient Events & Delegates - - /// - /// - /// - //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; - public event EventHandler ConnectionChange; - - /// - /// Auto reconnect evant handler - /// - public event EventHandler AutoReconnectTriggered; - - /// - /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. - /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. - /// - public event EventHandler TextReceivedQueueInvoke; - - /// - /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require - /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. - /// - public event EventHandler ClientReadyForCommunications; - - #endregion - - - #region GenricTcpIpClient properties - - private string _hostname; - - /// - /// Address of server - /// - public string Hostname - { - get { return _hostname; } - set - { - _hostname = value; - if (_client != null) - { - _client.AddressClientConnectedTo = _hostname; - } - } - } - - /// - /// Gets or sets the Port - /// - public int Port { get; set; } - - /// - /// S+ helper - /// - public ushort UPort - { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } - } - - /// - /// Defaults to 2000 - /// - public int BufferSize { get; set; } - - /// - /// Internal secure client - /// - private SecureTCPClient _client; - - /// - /// Bool showing if socket is connected - /// - public bool IsConnected - { - get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } - } - - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected - { - get { return (ushort)(IsConnected ? 1 : 0); } - } - - /// - /// _client socket status Read only - /// - public SocketStatus ClientStatus - { - get - { - return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; - } - } - - /// - /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event - /// and IsConnected would be true when this == 2. - /// - public ushort UStatus - { - get { return (ushort)ClientStatus; } - } - - /// - /// Status text shows the message associated with socket status - /// - public string ClientStatusText { get { return ClientStatus.ToString(); } } - - /// - /// Connection failure reason - /// - public string ConnectionFailure { get { return ClientStatus.ToString(); } } - - /// - /// Gets or sets the AutoReconnect - /// - public bool AutoReconnect { get; set; } - - /// - /// S+ helper for AutoReconnect - /// - public ushort UAutoReconnect - { - get { return (ushort)(AutoReconnect ? 1 : 0); } - set { AutoReconnect = value == 1; } - } - - /// - /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 - /// - public int AutoReconnectIntervalMs { get; set; } - - /// - /// Flag Set only when the disconnect method is called. - /// - bool DisconnectCalledByUser; - - /// - /// - /// - public bool Connected - { - get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } - } - - // private Timer for auto reconnect - private CTimer RetryTimer; - - #endregion - - #region GenericSecureTcpIpClient properties - - /// - /// Gets or sets the SharedKeyRequired - /// - public bool SharedKeyRequired { get; set; } - - /// - /// S+ helper for requires shared key bool - /// - public ushort USharedKeyRequired - { - set - { - if (value == 1) - SharedKeyRequired = true; - else - SharedKeyRequired = false; - } - } - - /// - /// Gets or sets the SharedKey - /// - public string SharedKey { get; set; } - - /// - /// flag to show the client is waiting for the server to send the shared key - /// - private bool WaitingForSharedKeyResponse { get; set; } - - /// - /// Semaphore on connect method - /// - bool IsTryingToConnect; - - /// - /// Gets or sets the IsReadyForCommunication - /// - public bool IsReadyForCommunication { get; set; } - - /// - /// S+ helper for IsReadyForCommunication - /// - public ushort UIsReadyForCommunication - { - get { return (ushort)(IsReadyForCommunication ? 1 : 0); } - } - - /// - /// Bool Heartbeat Enabled flag - /// - public bool HeartbeatEnabled { get; set; } - - /// - /// S+ helper for Heartbeat Enabled - /// - public ushort UHeartbeatEnabled - { - get { return (ushort)(HeartbeatEnabled ? 1 : 0); } - set { HeartbeatEnabled = value == 1; } - } - - /// - /// Heartbeat String - /// - public string HeartbeatString { get; set; } - //public int HeartbeatInterval = 50000; - - /// - /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ - /// - public int HeartbeatInterval { get; set; } - - /// - /// Simpl+ Heartbeat Analog value in seconds - /// - public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } - - CTimer HeartbeatSendTimer; - CTimer HeartbeatAckTimer; - - // Used to force disconnection on a dead connect attempt - CTimer ConnectFailTimer; - CTimer WaitForSharedKey; - private int ConnectionCount; - - bool ProgramIsStopping; - - /// - /// Queue lock - /// - CCriticalSection DequeueLock = new CCriticalSection(); - - /// - /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before - /// calling initialize. - /// - public int ReceiveQueueSize { get; set; } - - /// - /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before - /// calling initialize. - /// - private CrestronQueue MessageQueue; - - #endregion - - #region Constructors - - /// - /// Constructor - /// - /// - /// - /// - /// - public GenericSecureTcpIpClient(string key, string address, int port, int bufferSize) - : base(key) - { - StreamDebugging = new CommunicationStreamDebugging(key); - Hostname = address; - Port = port; - BufferSize = bufferSize; - AutoReconnectIntervalMs = 5000; - - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - } - - /// - /// Contstructor that sets all properties by calling the initialize method with a config object. - /// - /// - /// - public GenericSecureTcpIpClient(string key, TcpClientConfigObject clientConfigObject) - : base(key) - { - StreamDebugging = new CommunicationStreamDebugging(key); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; - BufferSize = 2000; - - Initialize(clientConfigObject); - } - - /// - /// Default constructor for S+ - /// - public GenericSecureTcpIpClient() - : base(SplusKey) - { - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; - BufferSize = 2000; - } - - /// - /// Initialize method - /// - public void Initialize(string key) - { - Key = key; - } - - /// - /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client. - /// - /// - public void Initialize(TcpClientConfigObject config) - { - if (config == null) - { - Debug.Console(0, this, "Could not initialize client with key: {0}", Key); - return; - } - try - { - Hostname = config.Control.TcpSshProperties.Address; - Port = config.Control.TcpSshProperties.Port > 0 && config.Control.TcpSshProperties.Port <= 65535 - ? config.Control.TcpSshProperties.Port - : 80; - - AutoReconnect = config.Control.TcpSshProperties.AutoReconnect; - AutoReconnectIntervalMs = config.Control.TcpSshProperties.AutoReconnectIntervalMs > 1000 - ? config.Control.TcpSshProperties.AutoReconnectIntervalMs - : 5000; - - SharedKey = config.SharedKey; - SharedKeyRequired = config.SharedKeyRequired; - - HeartbeatEnabled = config.HeartbeatRequired; - HeartbeatRequiredIntervalInSeconds = config.HeartbeatRequiredIntervalInSeconds > 0 - ? config.HeartbeatRequiredIntervalInSeconds - : (ushort)15; - - - HeartbeatString = string.IsNullOrEmpty(config.HeartbeatStringToMatch) - ? "heartbeat" - : config.HeartbeatStringToMatch; - - BufferSize = config.Control.TcpSshProperties.BufferSize > 2000 - ? config.Control.TcpSshProperties.BufferSize - : 2000; - - ReceiveQueueSize = config.ReceiveQueueSize > 20 - ? config.ReceiveQueueSize - : 20; - - MessageQueue = new CrestronQueue(ReceiveQueueSize); - } - catch (Exception ex) - { - Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex); - } - } - - #endregion - - /// - /// Handles closing this up when the program shuts down - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing _client connection"); - ProgramIsStopping = true; - Disconnect(); - } - - } - - /// - /// Deactivate the client - /// - /// - /// - /// Deactivate method - /// - public override bool Deactivate() + get { return _hostname; } + set { + _hostname = value; if (_client != null) { - _client.SocketStatusChange -= this.Client_SocketStatusChange; - DisconnectClient(); - } - return true; - } - - /// - /// Connect method - /// - public void Connect() - { - ConnectionCount++; - Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); - - - if (IsConnected) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); - return; - } - if (IsTryingToConnect) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); - return; - } - try - { - IsTryingToConnect = true; - if (RetryTimer != null) - { - RetryTimer.Stop(); - RetryTimer = null; - } - if (string.IsNullOrEmpty(Hostname)) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); - return; - } - if (Port < 1 || Port > 65535) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); - return; - } - if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); - return; - } - - // clean up previous client - if (_client != null) - { - Disconnect(); - } - DisconnectCalledByUser = false; - - _client = new SecureTCPClient(Hostname, Port, BufferSize); - _client.SocketStatusChange += Client_SocketStatusChange; - if (HeartbeatEnabled) - _client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); - _client.AddressClientConnectedTo = Hostname; - _client.PortNumber = Port; - // SecureClient = c; - - //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); - - ConnectFailTimer = new CTimer(o => - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); - if (IsTryingToConnect) - { - IsTryingToConnect = false; - - //if (ConnectionHasHungCallback != null) - //{ - // ConnectionHasHungCallback(); - //} - //SecureClient.DisconnectFromServer(); - //CheckClosedAndTryReconnect(); - } - }, 30000); - - Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); - _client.ConnectToServerAsync(o => - { - Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); - - if (ConnectFailTimer != null) - { - ConnectFailTimer.Stop(); - } - IsTryingToConnect = false; - - if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - { - Debug.Console(2, this, "_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); - o.ReceiveDataAsync(Receive); - - if (SharedKeyRequired) - { - WaitingForSharedKeyResponse = true; - WaitForSharedKey = new CTimer(timer => - { - - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); - // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); - // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup - o.DisconnectFromServer(); - //CheckClosedAndTryReconnect(); - //OnClientReadyForcommunications(false); // Should send false event - }, 15000); - } - else - { - //CLient connected and shared key is not required so just raise the ready for communication event. if Shared key - //required this is called by the shared key being negotiated - if (IsReadyForCommunication == false) - { - OnClientReadyForcommunications(true); // Key not required - } - } - } - else - { - Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); - CheckClosedAndTryReconnect(); - } - }); - } - catch (Exception ex) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "_client connection exception: {0}", ex.Message); - IsTryingToConnect = false; - CheckClosedAndTryReconnect(); + _client.AddressClientConnectedTo = _hostname; } } + } - /// - /// Disconnect method - /// - public void Disconnect() + /// + /// Port on server + /// + public int Port { get; set; } + + /// + /// S+ helper + /// + public ushort UPort + { + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + /// + /// Internal secure client + /// + private SecureTCPClient _client; + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// _client socket status Read only + /// + public SocketStatus ClientStatus + { + get { - this.LogVerbose("Disconnect Called"); + return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; + } + } - DisconnectCalledByUser = true; + /// + /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event + /// and IsConnected would be true when this == 2. + /// + public ushort UStatus + { + get { return (ushort)ClientStatus; } + } - // stop trying reconnects, if we are + /// + /// Status text shows the message associated with socket status + /// + public string ClientStatusText { get { return ClientStatus.ToString(); } } + + /// + /// Connection failure reason + /// + public string ConnectionFailure { get { return ClientStatus.ToString(); } } + + /// + /// bool to track if auto reconnect should be set on the socket + /// + public bool AutoReconnect { get; set; } + + /// + /// S+ helper for AutoReconnect + /// + public ushort UAutoReconnect + { + get { return (ushort)(AutoReconnect ? 1 : 0); } + set { AutoReconnect = value == 1; } + } + + /// + /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 + /// + public int AutoReconnectIntervalMs { get; set; } + + /// + /// Flag Set only when the disconnect method is called. + /// + bool DisconnectCalledByUser; + + /// + /// + /// + public bool Connected + { + get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + } + + // private Timer for auto reconnect + private CTimer RetryTimer; + + #endregion + + #region GenericSecureTcpIpClient properties + + /// + /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class + /// + public bool SharedKeyRequired { get; set; } + + /// + /// S+ helper for requires shared key bool + /// + public ushort USharedKeyRequired + { + set + { + if (value == 1) + SharedKeyRequired = true; + else + SharedKeyRequired = false; + } + } + + /// + /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module + /// + public string SharedKey { get; set; } + + /// + /// flag to show the client is waiting for the server to send the shared key + /// + private bool WaitingForSharedKeyResponse { get; set; } + + /// + /// Semaphore on connect method + /// + bool IsTryingToConnect; + + /// + /// Bool showing if socket is ready for communication after shared key exchange + /// + public bool IsReadyForCommunication { get; set; } + + /// + /// S+ helper for IsReadyForCommunication + /// + public ushort UIsReadyForCommunication + { + get { return (ushort)(IsReadyForCommunication ? 1 : 0); } + } + + /// + /// Bool Heartbeat Enabled flag + /// + public bool HeartbeatEnabled { get; set; } + + /// + /// S+ helper for Heartbeat Enabled + /// + public ushort UHeartbeatEnabled + { + get { return (ushort)(HeartbeatEnabled ? 1 : 0); } + set { HeartbeatEnabled = value == 1; } + } + + /// + /// Heartbeat String + /// + public string HeartbeatString { get; set; } + //public int HeartbeatInterval = 50000; + + /// + /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ + /// + public int HeartbeatInterval { get; set; } + + /// + /// Simpl+ Heartbeat Analog value in seconds + /// + public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } + + CTimer HeartbeatSendTimer; + CTimer HeartbeatAckTimer; + + // Used to force disconnection on a dead connect attempt + CTimer ConnectFailTimer; + CTimer WaitForSharedKey; + private int ConnectionCount; + + bool ProgramIsStopping; + + /// + /// Queue lock + /// + CCriticalSection DequeueLock = new CCriticalSection(); + + /// + /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before + /// calling initialize. + /// + public int ReceiveQueueSize { get; set; } + + /// + /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before + /// calling initialize. + /// + private CrestronQueue MessageQueue; + + #endregion + + #region Constructors + + /// + /// Constructor + /// + /// + /// + /// + /// + public GenericSecureTcpIpClient(string key, string address, int port, int bufferSize) + : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); + Hostname = address; + Port = port; + BufferSize = bufferSize; + AutoReconnectIntervalMs = 5000; + + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + } + + /// + /// Contstructor that sets all properties by calling the initialize method with a config object. + /// + /// + /// + public GenericSecureTcpIpClient(string key, TcpClientConfigObject clientConfigObject) + : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + + Initialize(clientConfigObject); + } + + /// + /// Default constructor for S+ + /// + public GenericSecureTcpIpClient() + : base(SplusKey) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + } + + /// + /// Just to help S+ set the key + /// + public void Initialize(string key) + { + Key = key; + } + + /// + /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client. + /// + /// + public void Initialize(TcpClientConfigObject config) + { + if (config == null) + { + this.LogWarning( "Could not initialize client with key: {0}", Key); + return; + } + try + { + Hostname = config.Control.TcpSshProperties.Address; + Port = config.Control.TcpSshProperties.Port > 0 && config.Control.TcpSshProperties.Port <= 65535 + ? config.Control.TcpSshProperties.Port + : 80; + + AutoReconnect = config.Control.TcpSshProperties.AutoReconnect; + AutoReconnectIntervalMs = config.Control.TcpSshProperties.AutoReconnectIntervalMs > 1000 + ? config.Control.TcpSshProperties.AutoReconnectIntervalMs + : 5000; + + SharedKey = config.SharedKey; + SharedKeyRequired = config.SharedKeyRequired; + + HeartbeatEnabled = config.HeartbeatRequired; + HeartbeatRequiredIntervalInSeconds = config.HeartbeatRequiredIntervalInSeconds > 0 + ? config.HeartbeatRequiredIntervalInSeconds + : (ushort)15; + + + HeartbeatString = string.IsNullOrEmpty(config.HeartbeatStringToMatch) + ? "heartbeat" + : config.HeartbeatStringToMatch; + + BufferSize = config.Control.TcpSshProperties.BufferSize > 2000 + ? config.Control.TcpSshProperties.BufferSize + : 2000; + + ReceiveQueueSize = config.ReceiveQueueSize > 20 + ? config.ReceiveQueueSize + : 20; + + MessageQueue = new CrestronQueue(ReceiveQueueSize); + } + catch (Exception ex) + { + this.LogError("Exception initializing client with key: {0}\rException: {1}", Key, ex); + } + } + + #endregion + + /// + /// Handles closing this up when the program shuts down + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) + { + this.LogInformation("Program stopping. Closing _client connection"); + ProgramIsStopping = true; + Disconnect(); + } + + } + + /// + /// Deactivate the client + /// + /// + public override bool Deactivate() + { + if (_client != null) + { + _client.SocketStatusChange -= this.Client_SocketStatusChange; + DisconnectClient(); + } + return true; + } + + /// + /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name. + /// + public void Connect() + { + ConnectionCount++; + this.LogVerbose("Attempting connect Count:{0}", ConnectionCount); + + + if (IsConnected) + { + this.LogInformation("Already connected. Ignoring."); + return; + } + if (IsTryingToConnect) + { + this.LogInformation("Already trying to connect. Ignoring."); + return; + } + try + { + IsTryingToConnect = true; if (RetryTimer != null) { RetryTimer.Stop(); RetryTimer = null; } - - if (_client != null) + if (string.IsNullOrEmpty(Hostname)) { - DisconnectClient(); - this.LogDebug("Disconnected"); + this.LogWarning("DynamicTcpClient: No address set"); + return; + } + if (Port < 1 || Port > 65535) + { + this.LogWarning("DynamicTcpClient: Invalid port"); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + this.LogWarning("DynamicTcpClient: No Shared Key set"); + return; } - } - /// - /// DisconnectClient method - /// - public void DisconnectClient() - { - if (_client == null) return; - - Debug.Console(1, this, "Disconnecting client"); - if (IsConnected) - _client.DisconnectFromServer(); - - // close up client. ALWAYS use this when disconnecting. - IsTryingToConnect = false; - - Debug.Console(2, this, "Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : ""); - _client.SocketStatusChange -= Client_SocketStatusChange; - _client.Dispose(); - _client = null; - - if (ConnectFailTimer == null) return; - ConnectFailTimer.Stop(); - ConnectFailTimer.Dispose(); - ConnectFailTimer = null; - } - - #region Methods - - /// - /// Called from Connect failure or Socket Status change if - /// auto reconnect and socket disconnected (Not disconnected by user) - /// - void CheckClosedAndTryReconnect() - { + // clean up previous client if (_client != null) { - Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); Disconnect(); } - if (!DisconnectCalledByUser && AutoReconnect) + DisconnectCalledByUser = false; + + _client = new SecureTCPClient(Hostname, Port, BufferSize); + _client.SocketStatusChange += Client_SocketStatusChange; + if (HeartbeatEnabled) + _client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); + _client.AddressClientConnectedTo = Hostname; + _client.PortNumber = Port; + // SecureClient = c; + + //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); + + ConnectFailTimer = new CTimer(o => { - var halfInterval = AutoReconnectIntervalMs / 2; - var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; - Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); - if (RetryTimer != null) + this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); + if (IsTryingToConnect) { - RetryTimer.Stop(); - RetryTimer = null; + IsTryingToConnect = false; + + //if (ConnectionHasHungCallback != null) + //{ + // ConnectionHasHungCallback(); + //} + //SecureClient.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); } - if (AutoReconnectTriggered != null) - AutoReconnectTriggered(this, new EventArgs()); - RetryTimer = new CTimer(o => Connect(), rndTime); - } - } + }, 30000); - /// - /// Receive callback - /// - /// - /// - void Receive(SecureTCPClient client, int numBytes) - { - if (numBytes > 0) + this.LogVerbose("Making Connection Count:{0}", ConnectionCount); + _client.ConnectToServerAsync(o => { - string str = string.Empty; - var handler = TextReceivedQueueInvoke; - try + this.LogVerbose("ConnectToServerAsync Count:{0} Ran!", ConnectionCount); + + if (ConnectFailTimer != null) { - var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); - str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - Debug.Console(2, this, "_client Received:\r--------\r{0}\r--------", str); - if (!string.IsNullOrEmpty(checkHeartbeat(str))) + ConnectFailTimer.Stop(); + } + IsTryingToConnect = false; + + if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + this.LogVerbose("_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); + o.ReceiveDataAsync(Receive); + + if (SharedKeyRequired) { - - if (SharedKeyRequired && str == "SharedKey:") + WaitingForSharedKeyResponse = true; + WaitForSharedKey = new CTimer(timer => { - Debug.Console(2, this, "Server asking for shared key, sending"); - SendText(SharedKey + "\n"); - } - else if (SharedKeyRequired && str == "Shared Key Match") - { - StopWaitForSharedKeyTimer(); - - Debug.Console(2, this, "Shared key confirmed. Ready for communication"); - OnClientReadyForcommunications(true); // Successful key exchange - } - else + this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); + // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); + // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup + o.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + //OnClientReadyForcommunications(false); // Should send false event + }, 15000); + } + else + { + //CLient connected and shared key is not required so just raise the ready for communication event. if Shared key + //required this is called by the shared key being negotiated + if (IsReadyForCommunication == false) { - //var bytesHandler = BytesReceived; - //if (bytesHandler != null) - // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - var textHandler = TextReceived; - if (textHandler != null) - textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - if (handler != null) - { - MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str)); - } + OnClientReadyForcommunications(true); // Key not required } } } - catch (Exception ex) + else { - Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); + this.LogWarning("Connect attempt failed {0}", o.ClientStatus); + CheckClosedAndTryReconnect(); } - if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - client.ReceiveDataAsync(Receive); + }); + } + catch (Exception ex) + { + this.LogError("_client connection exception: {0}", ex.Message); + IsTryingToConnect = false; + CheckClosedAndTryReconnect(); + } + } - //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. - if (handler != null) - { - var gotLock = DequeueLock.TryEnter(); - if (gotLock) - CrestronInvoke.BeginInvoke((o) => DequeueEvent()); - } - } - else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync - { - client.DisconnectFromServer(); - } + /// + /// + /// + public void Disconnect() + { + this.LogVerbose("Disconnect Called"); + + DisconnectCalledByUser = true; + + // stop trying reconnects, if we are + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; } - /// - /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. - /// It will dequeue items as they are enqueued automatically. - /// - void DequeueEvent() + if (_client != null) { + DisconnectClient(); + this.LogDebug("Disconnected"); + } + } + + /// + /// Does the actual disconnect business + /// + public void DisconnectClient() + { + if (_client == null) return; + + this.LogInformation("Disconnecting client"); + if (IsConnected) + _client.DisconnectFromServer(); + + // close up client. ALWAYS use this when disconnecting. + IsTryingToConnect = false; + + this.LogVerbose("Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : ""); + _client.SocketStatusChange -= Client_SocketStatusChange; + _client.Dispose(); + _client = null; + + if (ConnectFailTimer == null) return; + ConnectFailTimer.Stop(); + ConnectFailTimer.Dispose(); + ConnectFailTimer = null; + } + + #region Methods + + /// + /// Called from Connect failure or Socket Status change if + /// auto reconnect and socket disconnected (Not disconnected by user) + /// + void CheckClosedAndTryReconnect() + { + if (_client != null) + { + this.LogVerbose("Cleaning up remotely closed/failed connection."); + Disconnect(); + } + if (!DisconnectCalledByUser && AutoReconnect) + { + var halfInterval = AutoReconnectIntervalMs / 2; + var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; + this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime); + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + if (AutoReconnectTriggered != null) + AutoReconnectTriggered(this, new EventArgs()); + RetryTimer = new CTimer(o => Connect(), rndTime); + } + } + + /// + /// Receive callback + /// + /// + /// + void Receive(SecureTCPClient client, int numBytes) + { + if (numBytes > 0) + { + string str = string.Empty; + var handler = TextReceivedQueueInvoke; try { - while (true) + var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); + str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + this.LogVerbose("_client Received:\r--------\r{0}\r--------", str); + if (!string.IsNullOrEmpty(checkHeartbeat(str))) { - // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. - var message = MessageQueue.Dequeue(); - var handler = TextReceivedQueueInvoke; - if (handler != null) + + if (SharedKeyRequired && str == "SharedKey:") { - handler(this, message); + this.LogVerbose("Server asking for shared key, sending"); + SendText(SharedKey + "\n"); + } + else if (SharedKeyRequired && str == "Shared Key Match") + { + StopWaitForSharedKeyTimer(); + + + this.LogVerbose("Shared key confirmed. Ready for communication"); + OnClientReadyForcommunications(true); // Successful key exchange + } + else + { + //var bytesHandler = BytesReceived; + //if (bytesHandler != null) + // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var textHandler = TextReceived; + if (textHandler != null) + textHandler(this, new GenericCommMethodReceiveTextArgs(str)); + if (handler != null) + { + MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str)); + } } } } - catch (Exception e) + catch (Exception ex) { - this.LogException(e, "DequeueEvent error"); + this.LogError("Error receiving data: {1}. Error: {0}", str, ex.Message); } - // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. - if (DequeueLock != null) + if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + client.ReceiveDataAsync(Receive); + + //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. + if (handler != null) { - DequeueLock.Leave(); + var gotLock = DequeueLock.TryEnter(); + if (gotLock) + CrestronInvoke.BeginInvoke((o) => DequeueEvent()); + } + } + else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync + { + client.DisconnectFromServer(); + } + } + + /// + /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// It will dequeue items as they are enqueued automatically. + /// + void DequeueEvent() + { + try + { + while (true) + { + // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. + var message = MessageQueue.Dequeue(); + var handler = TextReceivedQueueInvoke; + if (handler != null) + { + handler(this, message); + } + } + } + catch (Exception e) + { + this.LogError(e, "DequeueEvent error: {0}", e.Message); + } + // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. + if (DequeueLock != null) + { + DequeueLock.Leave(); + } + } + + void HeartbeatStart() + { + if (HeartbeatEnabled) + { + this.LogVerbose("Starting Heartbeat"); + if (HeartbeatSendTimer == null) + { + + HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + } + if (HeartbeatAckTimer == null) + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); } } - void HeartbeatStart() + } + void HeartbeatStop() + { + + if (HeartbeatSendTimer != null) + { + this.LogVerbose("Stoping Heartbeat Send"); + HeartbeatSendTimer.Stop(); + HeartbeatSendTimer = null; + } + if (HeartbeatAckTimer != null) + { + this.LogVerbose("Stoping Heartbeat Ack"); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer = null; + } + + } + void SendHeartbeat(object notused) + { + this.SendText(HeartbeatString); + this.LogVerbose("Sending Heartbeat"); + + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(string received) + { + try { if (HeartbeatEnabled) { - this.LogVerbose("Starting Heartbeat"); - if (HeartbeatSendTimer == null) + if (!string.IsNullOrEmpty(HeartbeatString)) { - - HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); - } - if (HeartbeatAckTimer == null) - { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); - } - } - - } - void HeartbeatStop() - { - - if (HeartbeatSendTimer != null) - { - Debug.Console(2, this, "Stoping Heartbeat Send"); - HeartbeatSendTimer.Stop(); - HeartbeatSendTimer = null; - } - if (HeartbeatAckTimer != null) - { - Debug.Console(2, this, "Stoping Heartbeat Ack"); - HeartbeatAckTimer.Stop(); - HeartbeatAckTimer = null; - } - - } - void SendHeartbeat(object notused) - { - this.SendText(HeartbeatString); - Debug.Console(2, this, "Sending Heartbeat"); - - } - - //private method to check heartbeat requirements and start or reset timer - string checkHeartbeat(string received) - { - try - { - if (HeartbeatEnabled) - { - if (!string.IsNullOrEmpty(HeartbeatString)) + var remainingText = received.Replace(HeartbeatString, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatString)) { - var remainingText = received.Replace(HeartbeatString, ""); - var noDelimiter = received.Trim(new char[] { '\r', '\n' }); - if (noDelimiter.Contains(HeartbeatString)) + if (HeartbeatAckTimer != null) { - if (HeartbeatAckTimer != null) - { - HeartbeatAckTimer.Reset(HeartbeatInterval * 2); - } - else - { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); - } - Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); - return remainingText; + HeartbeatAckTimer.Reset(HeartbeatInterval * 2); } - } - } - } - catch (Exception ex) - { - Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); - } - return received; - } - - - - void HeartbeatAckTimerFail(object o) - { - try - { - - if (IsConnected) - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); - SendText("Heartbeat not received by server, closing connection"); - CheckClosedAndTryReconnect(); - } - - } - catch (Exception ex) - { - ErrorLog.Error("Heartbeat timeout Error on _client: {0}, {1}", Key, ex); - } - } - - /// - /// - /// - void StopWaitForSharedKeyTimer() - { - if (WaitForSharedKey != null) - { - WaitForSharedKey.Stop(); - WaitForSharedKey = null; - } - } - - /// - /// SendText method - /// - public void SendText(string text) - { - if (!string.IsNullOrEmpty(text)) - { - try - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - { - _client.SendDataAsync(bytes, bytes.Length, (c, n) => + else { - // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? - if (n <= 0) - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); - } - }); + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString); + return remainingText; } } - catch (Exception ex) - { - Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); - } } } - - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) + catch (Exception ex) { - if (bytes.Length > 0) - { - try - { - if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - _client.SendData(bytes, bytes.Length); - } - catch (Exception ex) - { - Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); - } - } + this.LogError(ex, "Error checking heartbeat: {0}", ex.Message); } - - /// - /// SocketStatusChange Callback - /// - /// - /// - void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus) - { - if (ProgramIsStopping) - { - ProgramIsStopping = false; - return; - } - try - { - Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); - - OnConnectionChange(); - // The client could be null or disposed by this time... - if (_client == null || _client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) - { - HeartbeatStop(); - OnClientReadyForcommunications(false); // socket has gone low - CheckClosedAndTryReconnect(); - } - } - catch (Exception ex) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); - } - } - - /// - /// Helper for ConnectionChange event - /// - void OnConnectionChange() - { - var handler = ConnectionChange; - if (handler == null) return; - - handler(this, new GenericSocketStatusChageEventArgs(this)); - } - - /// - /// Helper to fire ClientReadyForCommunications event - /// - void OnClientReadyForcommunications(bool isReady) - { - IsReadyForCommunication = isReady; - if (IsReadyForCommunication) - HeartbeatStart(); - - var handler = ClientReadyForCommunications; - if (handler == null) return; - - handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); - } - #endregion + return received; } + + + void HeartbeatAckTimerFail(object o) + { + try + { + + if (IsConnected) + { + this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); + SendText("Heartbeat not received by server, closing connection"); + CheckClosedAndTryReconnect(); + } + + } + catch (Exception ex) + { + this.LogError(ex, "Heartbeat timeout Error on _client: {0}, {1}", Key, ex.Message); + } + } + + /// + /// + /// + void StopWaitForSharedKeyTimer() + { + if (WaitForSharedKey != null) + { + WaitForSharedKey.Stop(); + WaitForSharedKey = null; + } + } + + /// + /// General send method + /// + public void SendText(string text) + { + if (!string.IsNullOrEmpty(text)) + { + try + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + _client.SendDataAsync(bytes, bytes.Length, (c, n) => + { + // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? + if (n <= 0) + { + this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key); + } + }); + } + } + catch (Exception ex) + { + this.LogError(ex, "Error sending text: {1}. Error: {0}", ex.Message, text); + } + } + } + + /// + /// + /// + public void SendBytes(byte[] bytes) + { + if (bytes.Length > 0) + { + try + { + if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + _client.SendData(bytes, bytes.Length); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending bytes. Error: {0}", ex.Message); + } + } + } + + /// + /// SocketStatusChange Callback + /// + /// + /// + void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus) + { + if (ProgramIsStopping) + { + ProgramIsStopping = false; + return; + } + try + { + this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); + + OnConnectionChange(); + // The client could be null or disposed by this time... + if (_client == null || _client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + HeartbeatStop(); + OnClientReadyForcommunications(false); // socket has gone low + CheckClosedAndTryReconnect(); + } + } + catch (Exception ex) + { + this.LogError(ex, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); + } + } + + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + var handler = ConnectionChange; + if (handler == null) return; + + handler(this, new GenericSocketStatusChageEventArgs(this)); + } + + /// + /// Helper to fire ClientReadyForCommunications event + /// + void OnClientReadyForcommunications(bool isReady) + { + IsReadyForCommunication = isReady; + if (IsReadyForCommunication) + HeartbeatStart(); + + var handler = ClientReadyForCommunications; + if (handler == null) return; + + handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); + } + #endregion } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs index 49350164..d46ce374 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs @@ -19,891 +19,890 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Generic secure TCP/IP client for server +/// +public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect { /// - /// Generic secure TCP/IP client for server + /// Band aid delegate for choked server /// - public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect + internal delegate void ConnectionHasHungCallbackDelegate(); + + #region Events + + //public event EventHandler BytesReceived; + + /// + /// Notifies of text received + /// + public event EventHandler TextReceived; + + /// + /// Notifies of auto reconnect sequence triggered + /// + public event EventHandler AutoReconnectTriggered; + + /// + /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. + /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. + /// + public event EventHandler TextReceivedQueueInvoke; + + /// + /// Notifies of socket status change + /// + public event EventHandler ConnectionChange; + + + /// + /// This is something of a band-aid callback. If the client times out during the connection process, because the server + /// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help + /// keep a watch on the server and reset it if necessary. + /// + internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback; + + /// + /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require + /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. + /// + public event EventHandler ClientReadyForCommunications; + + #endregion + + #region Properties & Variables + + /// + /// Address of server + /// + public string Hostname { get; set; } + + /// + /// The port number on which the server is listening. + /// + public int Port { get; set; } + + /// + /// S+ helper + /// + public ushort UPort { - /// - /// Band aid delegate for choked server - /// - internal delegate void ConnectionHasHungCallbackDelegate(); + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } - #region Events + /// + /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class + /// + public bool SharedKeyRequired { get; set; } - //public event EventHandler BytesReceived; - - /// - /// Notifies of text received - /// - public event EventHandler TextReceived; - - /// - /// Notifies of auto reconnect sequence triggered - /// - public event EventHandler AutoReconnectTriggered; - - /// - /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. - /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. - /// - public event EventHandler TextReceivedQueueInvoke; - - /// - /// Notifies of socket status change - /// - public event EventHandler ConnectionChange; - - - /// - /// This is something of a band-aid callback. If the client times out during the connection process, because the server - /// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help - /// keep a watch on the server and reset it if necessary. - /// - internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback; - - /// - /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require - /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. - /// - public event EventHandler ClientReadyForCommunications; - - #endregion - - #region Properties & Variables - - /// - /// Address of server - /// - public string Hostname { get; set; } - - /// - /// Gets or sets the Port - /// - public int Port { get; set; } - - /// - /// S+ helper - /// - public ushort UPort + /// + /// S+ helper for requires shared key bool + /// + public ushort USharedKeyRequired + { + set { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } + if (value == 1) + SharedKeyRequired = true; + else + SharedKeyRequired = false; } + } - /// - /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class - /// - public bool SharedKeyRequired { get; set; } + /// + /// SharedKey is sent for verification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module + /// + public string SharedKey { get; set; } - /// - /// S+ helper for requires shared key bool - /// - public ushort USharedKeyRequired + /// + /// flag to show the client is waiting for the server to send the shared key + /// + private bool WaitingForSharedKeyResponse { get; set; } + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + /// + /// Semaphore on connect method + /// + bool IsTryingToConnect; + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { - set + if (Client != null) + return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; + else + return false; + } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// Bool showing if socket is ready for communication after shared key exchange + /// + public bool IsReadyForCommunication { get; set; } + + /// + /// S+ helper for IsReadyForCommunication + /// + public ushort UIsReadyForCommunication + { + get { return (ushort)(IsReadyForCommunication ? 1 : 0); } + } + + /// + /// Client socket status Read only + /// + public SocketStatus ClientStatus + { + get + { + if (Client != null) + return Client.ClientStatus; + else + return SocketStatus.SOCKET_STATUS_NO_CONNECT; + } + } + + /// + /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event + /// and IsConnected would be true when this == 2. + /// + public ushort UStatus + { + get { return (ushort)ClientStatus; } + } + + /// + /// Status text shows the message associated with socket status + /// + public string ClientStatusText { get { return ClientStatus.ToString(); } } + + /// + /// bool to track if auto reconnect should be set on the socket + /// + public bool AutoReconnect { get; set; } + + /// + /// S+ helper for AutoReconnect + /// + public ushort UAutoReconnect + { + get { return (ushort)(AutoReconnect ? 1 : 0); } + set { AutoReconnect = value == 1; } + } + /// + /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 + /// + public int AutoReconnectIntervalMs { get; set; } + + /// + /// Flag Set only when the disconnect method is called. + /// + bool DisconnectCalledByUser; + + /// + /// private Timer for auto reconnect + /// + CTimer RetryTimer; + + + /// + /// + /// + public bool HeartbeatEnabled { get; set; } + /// + /// + /// + public ushort UHeartbeatEnabled + { + get { return (ushort)(HeartbeatEnabled ? 1 : 0); } + set { HeartbeatEnabled = value == 1; } + } + + /// + /// + /// + public string HeartbeatString { get; set; } + //public int HeartbeatInterval = 50000; + + /// + /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ + /// + public int HeartbeatInterval { get; set; } + + /// + /// Simpl+ Heartbeat Analog value in seconds + /// + public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } + + CTimer HeartbeatSendTimer; + CTimer HeartbeatAckTimer; + /// + /// Used to force disconnection on a dead connect attempt + /// + CTimer ConnectFailTimer; + CTimer WaitForSharedKey; + private int ConnectionCount; + /// + /// Internal secure client + /// + SecureTCPClient Client; + + bool ProgramIsStopping; + + /// + /// Queue lock + /// + CCriticalSection DequeueLock = new CCriticalSection(); + + /// + /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before + /// calling initialize. + /// + public int ReceiveQueueSize { get; set; } + + /// + /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before + /// calling initialize. + /// + private CrestronQueue MessageQueue; + + + #endregion + + #region Constructors + + /// + /// Constructor + /// + /// + /// + /// + /// + public GenericSecureTcpIpClient_ForServer(string key, string address, int port, int bufferSize) + : base(key) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Hostname = address; + Port = port; + BufferSize = bufferSize; + AutoReconnectIntervalMs = 5000; + + } + + /// + /// Constructor for S+ + /// + public GenericSecureTcpIpClient_ForServer() + : base("Uninitialized Secure Tcp Client For Server") + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + } + + /// + /// Contstructor that sets all properties by calling the initialize method with a config object. + /// + /// + /// + public GenericSecureTcpIpClient_ForServer(string key, TcpClientConfigObject clientConfigObject) + : base(key) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Initialize(clientConfigObject); + } + + #endregion + + #region Methods + + /// + /// Initializes the client's key property, which is used to identify this client instance. + /// + /// The unique key that identifies this client instance. + public void Initialize(string key) + { + Key = key; + } + + /// + /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client. + /// + /// The configuration object containing the client's settings. + public void Initialize(TcpClientConfigObject clientConfigObject) + { + try + { + if (clientConfigObject != null) { - if (value == 1) - SharedKeyRequired = true; - else - SharedKeyRequired = false; + var TcpSshProperties = clientConfigObject.Control.TcpSshProperties; + Hostname = TcpSshProperties.Address; + AutoReconnect = TcpSshProperties.AutoReconnect; + AutoReconnectIntervalMs = TcpSshProperties.AutoReconnectIntervalMs > 1000 ? + TcpSshProperties.AutoReconnectIntervalMs : 5000; + SharedKey = clientConfigObject.SharedKey; + SharedKeyRequired = clientConfigObject.SharedKeyRequired; + HeartbeatEnabled = clientConfigObject.HeartbeatRequired; + HeartbeatRequiredIntervalInSeconds = clientConfigObject.HeartbeatRequiredIntervalInSeconds > 0 ? + clientConfigObject.HeartbeatRequiredIntervalInSeconds : (ushort)15; + HeartbeatString = string.IsNullOrEmpty(clientConfigObject.HeartbeatStringToMatch) ? "heartbeat" : clientConfigObject.HeartbeatStringToMatch; + Port = TcpSshProperties.Port; + BufferSize = TcpSshProperties.BufferSize > 2000 ? TcpSshProperties.BufferSize : 2000; + ReceiveQueueSize = clientConfigObject.ReceiveQueueSize > 20 ? clientConfigObject.ReceiveQueueSize : 20; + MessageQueue = new CrestronQueue(ReceiveQueueSize); } - } - - /// - /// Gets or sets the SharedKey - /// - public string SharedKey { get; set; } - - /// - /// flag to show the client is waiting for the server to send the shared key - /// - private bool WaitingForSharedKeyResponse { get; set; } - - /// - /// Gets or sets the BufferSize - /// - public int BufferSize { get; set; } - - /// - /// Semaphore on connect method - /// - bool IsTryingToConnect; - - /// - /// Bool showing if socket is connected - /// - public bool IsConnected - { - get - { - if (Client != null) - return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; - else - return false; - } - } - - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected - { - get { return (ushort)(IsConnected ? 1 : 0); } - } - - /// - /// Bool showing if socket is ready for communication after shared key exchange - /// - public bool IsReadyForCommunication { get; set; } - - /// - /// S+ helper for IsReadyForCommunication - /// - public ushort UIsReadyForCommunication - { - get { return (ushort)(IsReadyForCommunication ? 1 : 0); } - } - - /// - /// Client socket status Read only - /// - public SocketStatus ClientStatus - { - get - { - if (Client != null) - return Client.ClientStatus; - else - return SocketStatus.SOCKET_STATUS_NO_CONNECT; - } - } - - /// - /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event - /// and IsConnected would be true when this == 2. - /// - public ushort UStatus - { - get { return (ushort)ClientStatus; } - } - - /// - /// Status text shows the message associated with socket status - /// - public string ClientStatusText { get { return ClientStatus.ToString(); } } - - /// - /// bool to track if auto reconnect should be set on the socket - /// - public bool AutoReconnect { get; set; } - - /// - /// S+ helper for AutoReconnect - /// - public ushort UAutoReconnect - { - get { return (ushort)(AutoReconnect ? 1 : 0); } - set { AutoReconnect = value == 1; } - } - /// - /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 - /// - public int AutoReconnectIntervalMs { get; set; } - - /// - /// Flag Set only when the disconnect method is called. - /// - bool DisconnectCalledByUser; - - /// - /// private Timer for auto reconnect - /// - CTimer RetryTimer; - - - /// - /// - /// - public bool HeartbeatEnabled { get; set; } - /// - /// - /// - public ushort UHeartbeatEnabled - { - get { return (ushort)(HeartbeatEnabled ? 1 : 0); } - set { HeartbeatEnabled = value == 1; } - } - - /// - /// - /// - public string HeartbeatString { get; set; } - //public int HeartbeatInterval = 50000; - - /// - /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ - /// - public int HeartbeatInterval { get; set; } - - /// - /// Simpl+ Heartbeat Analog value in seconds - /// - public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } - - CTimer HeartbeatSendTimer; - CTimer HeartbeatAckTimer; - /// - /// Used to force disconnection on a dead connect attempt - /// - CTimer ConnectFailTimer; - CTimer WaitForSharedKey; - private int ConnectionCount; - /// - /// Internal secure client - /// - SecureTCPClient Client; - - bool ProgramIsStopping; - - /// - /// Queue lock - /// - CCriticalSection DequeueLock = new CCriticalSection(); - - /// - /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before - /// calling initialize. - /// - public int ReceiveQueueSize { get; set; } - - /// - /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before - /// calling initialize. - /// - private CrestronQueue MessageQueue; - - - #endregion - - #region Constructors - - /// - /// Constructor - /// - /// - /// - /// - /// - public GenericSecureTcpIpClient_ForServer(string key, string address, int port, int bufferSize) - : base(key) - { - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - Hostname = address; - Port = port; - BufferSize = bufferSize; - AutoReconnectIntervalMs = 5000; - - } - - /// - /// Constructor for S+ - /// - public GenericSecureTcpIpClient_ForServer() - : base("Uninitialized Secure Tcp Client For Server") - { - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; - BufferSize = 2000; - } - - /// - /// Contstructor that sets all properties by calling the initialize method with a config object. - /// - /// - /// - public GenericSecureTcpIpClient_ForServer(string key, TcpClientConfigObject clientConfigObject) - : base(key) - { - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - Initialize(clientConfigObject); - } - - #endregion - - #region Methods - - /// - /// Initialize method - /// - public void Initialize(string key) - { - Key = key; - } - - /// - /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client. - /// - /// - public void Initialize(TcpClientConfigObject clientConfigObject) - { - try - { - if (clientConfigObject != null) - { - var TcpSshProperties = clientConfigObject.Control.TcpSshProperties; - Hostname = TcpSshProperties.Address; - AutoReconnect = TcpSshProperties.AutoReconnect; - AutoReconnectIntervalMs = TcpSshProperties.AutoReconnectIntervalMs > 1000 ? - TcpSshProperties.AutoReconnectIntervalMs : 5000; - SharedKey = clientConfigObject.SharedKey; - SharedKeyRequired = clientConfigObject.SharedKeyRequired; - HeartbeatEnabled = clientConfigObject.HeartbeatRequired; - HeartbeatRequiredIntervalInSeconds = clientConfigObject.HeartbeatRequiredIntervalInSeconds > 0 ? - clientConfigObject.HeartbeatRequiredIntervalInSeconds : (ushort)15; - HeartbeatString = string.IsNullOrEmpty(clientConfigObject.HeartbeatStringToMatch) ? "heartbeat" : clientConfigObject.HeartbeatStringToMatch; - Port = TcpSshProperties.Port; - BufferSize = TcpSshProperties.BufferSize > 2000 ? TcpSshProperties.BufferSize : 2000; - ReceiveQueueSize = clientConfigObject.ReceiveQueueSize > 20 ? clientConfigObject.ReceiveQueueSize : 20; - MessageQueue = new CrestronQueue(ReceiveQueueSize); - } - else - { - ErrorLog.Error("Could not initialize client with key: {0}", Key); - } - } - catch + else { ErrorLog.Error("Could not initialize client with key: {0}", Key); } } - - /// - /// Handles closing this up when the program shuts down - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + catch { - if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection"); - ProgramIsStopping = true; - Disconnect(); - } + ErrorLog.Error("Could not initialize client with key: {0}", Key); + } + } + /// + /// Handles closing this up when the program shuts down + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) + { + this.LogInformation("Program stopping. Closing Client connection"); + ProgramIsStopping = true; + Disconnect(); } - /// - /// Connect method - /// - public void Connect() + } + + /// + /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name. + /// + public void Connect() + { + ConnectionCount++; + this.LogVerbose("Attempting connect Count:{0}", ConnectionCount); + + + if (IsConnected) { - ConnectionCount++; - Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); - - - if (IsConnected) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); - return; - } - if (IsTryingToConnect) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); - return; - } - try - { - IsTryingToConnect = true; - if (RetryTimer != null) - { - RetryTimer.Stop(); - RetryTimer = null; - } - if (string.IsNullOrEmpty(Hostname)) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); - return; - } - if (Port < 1 || Port > 65535) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); - return; - } - if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); - return; - } - - // clean up previous client - if (Client != null) - { - Cleanup(); - } - DisconnectCalledByUser = false; - - Client = new SecureTCPClient(Hostname, Port, BufferSize); - Client.SocketStatusChange += Client_SocketStatusChange; - if (HeartbeatEnabled) - Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); - Client.AddressClientConnectedTo = Hostname; - Client.PortNumber = Port; - // SecureClient = c; - - //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); - - ConnectFailTimer = new CTimer(o => - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); - if (IsTryingToConnect) - { - IsTryingToConnect = false; - - //if (ConnectionHasHungCallback != null) - //{ - // ConnectionHasHungCallback(); - //} - //SecureClient.DisconnectFromServer(); - //CheckClosedAndTryReconnect(); - } - }, 30000); - - Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); - Client.ConnectToServerAsync(o => - { - Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); - - if (ConnectFailTimer != null) - { - ConnectFailTimer.Stop(); - } - IsTryingToConnect = false; - - if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - { - Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); - o.ReceiveDataAsync(Receive); - - if (SharedKeyRequired) - { - WaitingForSharedKeyResponse = true; - WaitForSharedKey = new CTimer(timer => - { - - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); - // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); - // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup - o.DisconnectFromServer(); - //CheckClosedAndTryReconnect(); - //OnClientReadyForcommunications(false); // Should send false event - }, 15000); - } - else - { - //CLient connected and shared key is not required so just raise the ready for communication event. if Shared key - //required this is called by the shared key being negotiated - if (IsReadyForCommunication == false) - { - OnClientReadyForcommunications(true); // Key not required - } - } - } - else - { - Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); - CheckClosedAndTryReconnect(); - } - }); - } - catch (Exception ex) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message); - IsTryingToConnect = false; - CheckClosedAndTryReconnect(); - } + this.LogInformation("Already connected. Ignoring."); + return; } - - /// - /// Disconnect method - /// - public void Disconnect() + if (IsTryingToConnect) { - this.LogVerbose("Disconnect Called"); - - DisconnectCalledByUser = true; - if (IsConnected) - { - Client.DisconnectFromServer(); - - } + this.LogInformation("Already trying to connect. Ignoring."); + return; + } + try + { + IsTryingToConnect = true; if (RetryTimer != null) { RetryTimer.Stop(); RetryTimer = null; } - Cleanup(); - } - - /// - /// Internal call to close up client. ALWAYS use this when disconnecting. - /// - void Cleanup() - { - IsTryingToConnect = false; + if (string.IsNullOrEmpty(Hostname)) + { + this.LogWarning("DynamicTcpClient: No address set"); + return; + } + if (Port < 1 || Port > 65535) + { + this.LogWarning("DynamicTcpClient: Invalid port"); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + this.LogWarning("DynamicTcpClient: No Shared Key set"); + return; + } + // clean up previous client if (Client != null) { - //SecureClient.DisconnectFromServer(); - Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); - Client.SocketStatusChange -= Client_SocketStatusChange; - Client.Dispose(); - Client = null; - } - if (ConnectFailTimer != null) - { - ConnectFailTimer.Stop(); - ConnectFailTimer.Dispose(); - ConnectFailTimer = null; - } - } - - - /// ff - /// Called from Connect failure or Socket Status change if - /// auto reconnect and socket disconnected (Not disconnected by user) - /// - void CheckClosedAndTryReconnect() - { - if (Client != null) - { - Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); Cleanup(); } - if (!DisconnectCalledByUser && AutoReconnect) + DisconnectCalledByUser = false; + + Client = new SecureTCPClient(Hostname, Port, BufferSize); + Client.SocketStatusChange += Client_SocketStatusChange; + if (HeartbeatEnabled) + Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); + Client.AddressClientConnectedTo = Hostname; + Client.PortNumber = Port; + // SecureClient = c; + + //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); + + ConnectFailTimer = new CTimer(o => { - var halfInterval = AutoReconnectIntervalMs / 2; - var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; - Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); - if (RetryTimer != null) + this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); + if (IsTryingToConnect) { - RetryTimer.Stop(); - RetryTimer = null; + IsTryingToConnect = false; + + //if (ConnectionHasHungCallback != null) + //{ + // ConnectionHasHungCallback(); + //} + //SecureClient.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); } - if(AutoReconnectTriggered != null) - AutoReconnectTriggered(this, new EventArgs()); - RetryTimer = new CTimer(o => Connect(), rndTime); - } - } + }, 30000); - /// - /// Receive callback - /// - /// - /// - void Receive(SecureTCPClient client, int numBytes) - { - if (numBytes > 0) + this.LogVerbose("Making Connection Count:{0}", ConnectionCount); + Client.ConnectToServerAsync(o => { - string str = string.Empty; - var handler = TextReceivedQueueInvoke; - try + this.LogVerbose("ConnectToServerAsync Count:{0} Ran!", ConnectionCount); + + if (ConnectFailTimer != null) { - var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); - str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); - if (!string.IsNullOrEmpty(checkHeartbeat(str))) + ConnectFailTimer.Stop(); + } + IsTryingToConnect = false; + + if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); + o.ReceiveDataAsync(Receive); + + if (SharedKeyRequired) { - - if (SharedKeyRequired && str == "SharedKey:") + WaitingForSharedKeyResponse = true; + WaitForSharedKey = new CTimer(timer => { - Debug.Console(2, this, "Server asking for shared key, sending"); - SendText(SharedKey + "\n"); - } - else if (SharedKeyRequired && str == "Shared Key Match") - { - StopWaitForSharedKeyTimer(); - - Debug.Console(2, this, "Shared key confirmed. Ready for communication"); - OnClientReadyForcommunications(true); // Successful key exchange - } - else + this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); + // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); + // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup + o.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + //OnClientReadyForcommunications(false); // Should send false event + }, 15000); + } + else + { + //CLient connected and shared key is not required so just raise the ready for communication event. if Shared key + //required this is called by the shared key being negotiated + if (IsReadyForCommunication == false) { - //var bytesHandler = BytesReceived; - //if (bytesHandler != null) - // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - var textHandler = TextReceived; - if (textHandler != null) - textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str)); - if (handler != null) - { - MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str)); - } + OnClientReadyForcommunications(true); // Key not required } } } - catch (Exception ex) + else { - Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); + this.LogWarning("Connect attempt failed {0}", o.ClientStatus); + CheckClosedAndTryReconnect(); } - if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - client.ReceiveDataAsync(Receive); - - //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. - if (handler != null) - { - var gotLock = DequeueLock.TryEnter(); - if (gotLock) - CrestronInvoke.BeginInvoke((o) => DequeueEvent()); - } - } - else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync - { - client.DisconnectFromServer(); - } + }); } - - /// - /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. - /// It will dequeue items as they are enqueued automatically. - /// - void DequeueEvent() + catch (Exception ex) { + this.LogError("Client connection exception: {0}", ex.Message); + IsTryingToConnect = false; + CheckClosedAndTryReconnect(); + } + } + + /// + /// + /// + public void Disconnect() + { + this.LogVerbose("Disconnect Called"); + + DisconnectCalledByUser = true; + if (IsConnected) + { + Client.DisconnectFromServer(); + + } + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + Cleanup(); + } + + /// + /// Internal call to close up client. ALWAYS use this when disconnecting. + /// + void Cleanup() + { + IsTryingToConnect = false; + + if (Client != null) + { + //SecureClient.DisconnectFromServer(); + this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); + Client.SocketStatusChange -= Client_SocketStatusChange; + Client.Dispose(); + Client = null; + } + if (ConnectFailTimer != null) + { + ConnectFailTimer.Stop(); + ConnectFailTimer.Dispose(); + ConnectFailTimer = null; + } + } + + + /// ff + /// Called from Connect failure or Socket Status change if + /// auto reconnect and socket disconnected (Not disconnected by user) + /// + void CheckClosedAndTryReconnect() + { + if (Client != null) + { + this.LogVerbose("Cleaning up remotely closed/failed connection."); + Cleanup(); + } + if (!DisconnectCalledByUser && AutoReconnect) + { + var halfInterval = AutoReconnectIntervalMs / 2; + var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; + this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime); + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + if (AutoReconnectTriggered != null) + AutoReconnectTriggered(this, new EventArgs()); + RetryTimer = new CTimer(o => Connect(), rndTime); + } + } + + /// + /// Receive callback + /// + /// + /// + void Receive(SecureTCPClient client, int numBytes) + { + if (numBytes > 0) + { + string str = string.Empty; + var handler = TextReceivedQueueInvoke; try { - while (true) + var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); + str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + this.LogVerbose("Client Received:\r--------\r{0}\r--------", str); + if (!string.IsNullOrEmpty(checkHeartbeat(str))) { - // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. - var message = MessageQueue.Dequeue(); - var handler = TextReceivedQueueInvoke; - if (handler != null) + + if (SharedKeyRequired && str == "SharedKey:") { - handler(this, message); + this.LogVerbose("Server asking for shared key, sending"); + SendText(SharedKey + "\n"); + } + else if (SharedKeyRequired && str == "Shared Key Match") + { + StopWaitForSharedKeyTimer(); + + + this.LogVerbose("Shared key confirmed. Ready for communication"); + OnClientReadyForcommunications(true); // Successful key exchange + } + else + { + //var bytesHandler = BytesReceived; + //if (bytesHandler != null) + // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var textHandler = TextReceived; + if (textHandler != null) + textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str)); + if (handler != null) + { + MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str)); + } } } } - catch (Exception e) + catch (Exception ex) { - this.LogException(e, "DequeueEvent error"); + this.LogError("Error receiving data: {1}. Error: {0}", ex.Message, str); } - // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. - if (DequeueLock != null) + if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + client.ReceiveDataAsync(Receive); + + //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. + if (handler != null) { - DequeueLock.Leave(); + var gotLock = DequeueLock.TryEnter(); + if (gotLock) + CrestronInvoke.BeginInvoke((o) => DequeueEvent()); + } + } + else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync + { + client.DisconnectFromServer(); + } + } + + /// + /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// It will dequeue items as they are enqueued automatically. + /// + void DequeueEvent() + { + try + { + while (true) + { + // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. + var message = MessageQueue.Dequeue(); + var handler = TextReceivedQueueInvoke; + if (handler != null) + { + handler(this, message); + } + } + } + catch (Exception e) + { + this.LogError("DequeueEvent error: {0}", e.Message, e); + } + // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. + if (DequeueLock != null) + { + DequeueLock.Leave(); + } + } + + void HeartbeatStart() + { + if (HeartbeatEnabled) + { + this.LogVerbose("Starting Heartbeat"); + if (HeartbeatSendTimer == null) + { + + HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + } + if (HeartbeatAckTimer == null) + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); } } - void HeartbeatStart() + } + void HeartbeatStop() + { + + if (HeartbeatSendTimer != null) + { + this.LogVerbose("Stopping Heartbeat Send"); + HeartbeatSendTimer.Stop(); + HeartbeatSendTimer = null; + } + if (HeartbeatAckTimer != null) + { + this.LogVerbose("Stopping Heartbeat Ack"); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer = null; + } + + } + void SendHeartbeat(object notused) + { + this.SendText(HeartbeatString); + this.LogVerbose("Sending Heartbeat"); + + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(string received) + { + try { if (HeartbeatEnabled) { - Debug.Console(2, this, "Starting Heartbeat"); - if (HeartbeatSendTimer == null) + if (!string.IsNullOrEmpty(HeartbeatString)) { - - HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); - } - if (HeartbeatAckTimer == null) - { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); - } - } - - } - void HeartbeatStop() - { - - if (HeartbeatSendTimer != null) - { - Debug.Console(2, this, "Stoping Heartbeat Send"); - HeartbeatSendTimer.Stop(); - HeartbeatSendTimer = null; - } - if (HeartbeatAckTimer != null) - { - Debug.Console(2, this, "Stoping Heartbeat Ack"); - HeartbeatAckTimer.Stop(); - HeartbeatAckTimer = null; - } - - } - void SendHeartbeat(object notused) - { - this.SendText(HeartbeatString); - Debug.Console(2, this, "Sending Heartbeat"); - - } - - //private method to check heartbeat requirements and start or reset timer - string checkHeartbeat(string received) - { - try - { - if (HeartbeatEnabled) - { - if (!string.IsNullOrEmpty(HeartbeatString)) + var remainingText = received.Replace(HeartbeatString, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatString)) { - var remainingText = received.Replace(HeartbeatString, ""); - var noDelimiter = received.Trim(new char[] { '\r', '\n' }); - if (noDelimiter.Contains(HeartbeatString)) + if (HeartbeatAckTimer != null) { - if (HeartbeatAckTimer != null) - { - HeartbeatAckTimer.Reset(HeartbeatInterval * 2); - } - else - { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); - } - Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); - return remainingText; + HeartbeatAckTimer.Reset(HeartbeatInterval * 2); } - } - } - } - catch (Exception ex) - { - Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); - } - return received; - } - - - - void HeartbeatAckTimerFail(object o) - { - try - { - - if (IsConnected) - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); - SendText("Heartbeat not received by server, closing connection"); - CheckClosedAndTryReconnect(); - } - - } - catch (Exception ex) - { - ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); - } - } - - /// - /// - /// - void StopWaitForSharedKeyTimer() - { - if (WaitForSharedKey != null) - { - WaitForSharedKey.Stop(); - WaitForSharedKey = null; - } - } - - /// - /// SendText method - /// - public void SendText(string text) - { - if (!string.IsNullOrEmpty(text)) - { - try - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - { - Client.SendDataAsync(bytes, bytes.Length, (c, n) => + else { - // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? - if (n <= 0) - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); - } - }); + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString); + return remainingText; } } - catch (Exception ex) - { - Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); - } } } - - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) + catch (Exception ex) { - if (bytes.Length > 0) - { - try - { - if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - Client.SendData(bytes, bytes.Length); - } - catch (Exception ex) - { - Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); - } - } + this.LogError("Error checking heartbeat: {0}", ex.Message, ex); } - - /// - /// SocketStatusChange Callback - /// - /// - /// - void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus) - { - if (ProgramIsStopping) - { - ProgramIsStopping = false; - return; - } - try - { - Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); - - OnConnectionChange(); - // The client could be null or disposed by this time... - if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) - { - HeartbeatStop(); - OnClientReadyForcommunications(false); // socket has gone low - CheckClosedAndTryReconnect(); - } - } - catch (Exception ex) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); - } - } - - /// - /// Helper for ConnectionChange event - /// - void OnConnectionChange() - { - var handler = ConnectionChange; - if (handler != null) - ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus)); - } - - /// - /// Helper to fire ClientReadyForCommunications event - /// - void OnClientReadyForcommunications(bool isReady) - { - IsReadyForCommunication = isReady; - if (this.IsReadyForCommunication) { HeartbeatStart(); } - var handler = ClientReadyForCommunications; - if (handler != null) - handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); - } - #endregion + return received; } + + + void HeartbeatAckTimerFail(object o) + { + try + { + + if (IsConnected) + { + this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); + SendText("Heartbeat not received by server, closing connection"); + CheckClosedAndTryReconnect(); + } + + } + catch (Exception ex) + { + this.LogError("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); + } + } + + /// + /// + /// + void StopWaitForSharedKeyTimer() + { + if (WaitForSharedKey != null) + { + WaitForSharedKey.Stop(); + WaitForSharedKey = null; + } + } + + /// + /// General send method + /// + public void SendText(string text) + { + if (!string.IsNullOrEmpty(text)) + { + try + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + Client.SendDataAsync(bytes, bytes.Length, (c, n) => + { + // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? + if (n <= 0) + { + this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key); + } + }); + } + } + catch (Exception ex) + { + this.LogError("Error sending text: {1}. Error: {0}", text, ex.Message); + } + } + } + + /// + /// + /// + public void SendBytes(byte[] bytes) + { + if (bytes.Length > 0) + { + try + { + if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + Client.SendData(bytes, bytes.Length); + } + catch (Exception ex) + { + this.LogError("Error sending bytes. Error: {0}", ex.Message, ex); + } + } + } + + /// + /// SocketStatusChange Callback + /// + /// + /// + void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus) + { + if (ProgramIsStopping) + { + ProgramIsStopping = false; + return; + } + try + { + this.LogDebug("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); + + OnConnectionChange(); + // The client could be null or disposed by this time... + if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + HeartbeatStop(); + OnClientReadyForcommunications(false); // socket has gone low + CheckClosedAndTryReconnect(); + } + } + catch (Exception ex) + { + this.LogError("Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); + } + } + + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + var handler = ConnectionChange; + if (handler != null) + ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus)); + } + + /// + /// Helper to fire ClientReadyForCommunications event + /// + void OnClientReadyForcommunications(bool isReady) + { + IsReadyForCommunication = isReady; + if (this.IsReadyForCommunication) { HeartbeatStart(); } + var handler = ClientReadyForCommunications; + if (handler != null) + handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); + } + #endregion } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs index 6502d544..7fed3948 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs @@ -17,405 +17,403 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Generic secure TCP/IP server +/// +public class GenericSecureTcpIpServer : Device { + #region Events /// - /// Generic secure TCP/IP server + /// Event for Receiving text /// - public class GenericSecureTcpIpServer : Device + public event EventHandler TextReceived; + + /// + /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. + /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. + /// + public event EventHandler TextReceivedQueueInvoke; + + /// + /// Event for client connection socket status change + /// + public event EventHandler ClientConnectionChange; + + /// + /// Event for Server State Change + /// + public event EventHandler ServerStateChange; + + /// + /// For a server with a pre shared key, this will fire after the communication is established and the key exchange is complete. If no shared key, this will fire + /// after connection is successful. Use this event to know when the client is ready for communication to avoid stepping on shared key. + /// + public event EventHandler ServerClientReadyForCommunications; + + /// + /// A band aid event to notify user that the server has choked. + /// + public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; } + + /// + /// + /// + public delegate void ServerHasChokedCallbackDelegate(); + + #endregion + + #region Properties/Variables + + /// + /// Server listen lock + /// + CCriticalSection ServerCCSection = new CCriticalSection(); + + /// + /// Queue lock + /// + CCriticalSection DequeueLock = new CCriticalSection(); + + /// + /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before + /// calling initialize. + /// + public int ReceiveQueueSize { get; set; } + + /// + /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before + /// calling initialize. + /// + private CrestronQueue MessageQueue; + + /// + /// A bandaid client that monitors whether the server is reachable + /// + GenericSecureTcpIpClient_ForServer MonitorClient; + + /// + /// Timer to operate the bandaid monitor client in a loop. + /// + CTimer MonitorClientTimer; + + /// + /// + /// + int MonitorClientFailureCount; + + /// + /// 3 by default + /// + public int MonitorClientMaxFailureCount { get; set; } + + /// + /// Text representation of the Socket Status enum values for the server + /// + public string Status { - #region Events - /// - /// Event for Receiving text - /// - public event EventHandler TextReceived; - - /// - /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. - /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. - /// - public event EventHandler TextReceivedQueueInvoke; - - /// - /// Event for client connection socket status change - /// - public event EventHandler ClientConnectionChange; - - /// - /// Event for Server State Change - /// - public event EventHandler ServerStateChange; - - /// - /// For a server with a pre shared key, this will fire after the communication is established and the key exchange is complete. If no shared key, this will fire - /// after connection is successful. Use this event to know when the client is ready for communication to avoid stepping on shared key. - /// - public event EventHandler ServerClientReadyForCommunications; - - /// - /// A band aid event to notify user that the server has choked. - /// - public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; } - - /// - /// Delegate for ServerHasChokedCallbackDelegate - /// - public delegate void ServerHasChokedCallbackDelegate(); - - #endregion - - #region Properties/Variables - - /// - /// Server listen lock - /// - CCriticalSection ServerCCSection = new CCriticalSection(); - - /// - /// Queue lock - /// - CCriticalSection DequeueLock = new CCriticalSection(); - - /// - /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before - /// calling initialize. - /// - public int ReceiveQueueSize { get; set; } - - /// - /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before - /// calling initialize. - /// - private CrestronQueue MessageQueue; - - /// - /// A bandaid client that monitors whether the server is reachable - /// - GenericSecureTcpIpClient_ForServer MonitorClient; - - /// - /// Timer to operate the bandaid monitor client in a loop. - /// - CTimer MonitorClientTimer; - - /// - /// - /// - int MonitorClientFailureCount; - - /// - /// Gets or sets the MonitorClientMaxFailureCount - /// - public int MonitorClientMaxFailureCount { get; set; } - - /// - /// Text representation of the Socket Status enum values for the server - /// - public string Status + get { - get - { - if (SecureServer != null) - return SecureServer.State.ToString(); - return ServerState.SERVER_NOT_LISTENING.ToString(); - - } + if (SecureServer != null) + return SecureServer.State.ToString(); + return ServerState.SERVER_NOT_LISTENING.ToString(); } - /// - /// Bool showing if socket is connected - /// - public bool IsConnected + } + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { - get - { - if (SecureServer != null) - return (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED; + if (SecureServer != null) + return (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED; + return false; + + //return (Secure ? SecureServer != null : UnsecureServer != null) && + //(Secure ? (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED : + // (UnsecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED); + } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// Bool showing if socket is connected + /// + public bool IsListening + { + get + { + if (SecureServer != null) + return (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING; + else return false; - - //return (Secure ? SecureServer != null : UnsecureServer != null) && - //(Secure ? (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED : - // (UnsecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED); - } + //return (Secure ? SecureServer != null : UnsecureServer != null) && + //(Secure ? (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING : + // (UnsecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING); } + } - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected + /// + /// S+ helper for IsConnected + /// + public ushort UIsListening + { + get { return (ushort)(IsListening ? 1 : 0); } + } + /// + /// Max number of clients this server will allow for connection. Crestron max is 64. This number should be less than 65 + /// + public ushort MaxClients { get; set; } // should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable + /// + /// Number of clients currently connected. + /// + public ushort NumberOfClientsConnected + { + get { - get { return (ushort)(IsConnected ? 1 : 0); } + if (SecureServer != null) + return (ushort)SecureServer.NumberOfClientsConnected; + return 0; } + } - /// - /// Bool showing if socket is connected - /// - public bool IsListening + /// + /// Port Server should listen on + /// + public int Port { get; set; } + + /// + /// S+ helper for Port + /// + public ushort UPort + { + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } + + /// + /// Bool to show whether the server requires a preshared key. Must be set the same in the client, and if true shared keys must be identical on server/client + /// + public bool SharedKeyRequired { get; set; } + + /// + /// S+ helper for requires shared key bool + /// + public ushort USharedKeyRequired + { + set { - get + if (value == 1) + SharedKeyRequired = true; + else + SharedKeyRequired = false; + } + } + + /// + /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module. + /// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called + /// + public string SharedKey { get; set; } + + /// + /// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received + /// + public bool HeartbeatRequired { get; set; } + + /// + /// S+ Helper for Heartbeat Required + /// + public ushort UHeartbeatRequired + { + set + { + if (value == 1) + HeartbeatRequired = true; + else + HeartbeatRequired = false; + } + } + + /// + /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ + /// + public int HeartbeatRequiredIntervalMs { get; set; } + + /// + /// Simpl+ Heartbeat Analog value in seconds + /// + public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } } + + /// + /// String to Match for heartbeat. If null or empty any string will reset heartbeat timer + /// + public string HeartbeatStringToMatch { get; set; } + + //private timers for Heartbeats per client + Dictionary HeartbeatTimerDictionary = new Dictionary(); + + //flags to show the secure server is waiting for client at index to send the shared key + List WaitingForSharedKey = new List(); + + List ClientReadyAfterKeyExchange = new List(); + + /// + /// The connected client indexes + /// + public List ConnectedClientsIndexes = new List(); + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + /// + /// Private flag to note that the server has stopped intentionally + /// + private bool ServerStopped { get; set; } + + //Servers + SecureTCPServer SecureServer; + + /// + /// + /// + bool ProgramIsStopping; + + #endregion + + #region Constructors + /// + /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually. + /// + public GenericSecureTcpIpServer() + : base("Uninitialized Secure TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + } + + /// + /// constructor with debug key set at instantiation. Make sure to set all properties before listening. + /// + /// + public GenericSecureTcpIpServer(string key) + : base("Uninitialized Secure TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + Key = key; + } + + /// + /// Contstructor that sets all properties by calling the initialize method with a config object. This does set Queue size. + /// + /// + public GenericSecureTcpIpServer(TcpServerConfigObject serverConfigObject) + : base("Uninitialized Secure TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + Initialize(serverConfigObject); + } + #endregion + + #region Methods - Server Actions + /// + /// Disconnects all clients and stops the server + /// + public void KillServer() + { + ServerStopped = true; + if (MonitorClient != null) + { + MonitorClient.Disconnect(); + } + DisconnectAllClientsForShutdown(); + StopListening(); + } + + /// + /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ + /// + /// + public void Initialize(string key) + { + Key = key; + } + + /// + /// Initialze the server + /// + /// + public void Initialize(TcpServerConfigObject serverConfigObject) + { + try + { + if (serverConfigObject != null || string.IsNullOrEmpty(serverConfigObject.Key)) { - if (SecureServer != null) - return (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING; - else - return false; - //return (Secure ? SecureServer != null : UnsecureServer != null) && - //(Secure ? (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING : - // (UnsecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING); + Key = serverConfigObject.Key; + MaxClients = serverConfigObject.MaxClients; + Port = serverConfigObject.Port; + SharedKeyRequired = serverConfigObject.SharedKeyRequired; + SharedKey = serverConfigObject.SharedKey; + HeartbeatRequired = serverConfigObject.HeartbeatRequired; + HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds; + HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch; + BufferSize = serverConfigObject.BufferSize; + ReceiveQueueSize = serverConfigObject.ReceiveQueueSize > 20 ? serverConfigObject.ReceiveQueueSize : 20; + MessageQueue = new CrestronQueue(ReceiveQueueSize); } - } - - /// - /// S+ helper for IsConnected - /// - public ushort UIsListening - { - get { return (ushort)(IsListening ? 1 : 0); } - } - /// - /// Max number of clients this server will allow for connection. Crestron max is 64. This number should be less than 65 - /// - public ushort MaxClients { get; set; } // should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable - /// - /// Number of clients currently connected. - /// - public ushort NumberOfClientsConnected - { - get - { - if (SecureServer != null) - return (ushort)SecureServer.NumberOfClientsConnected; - return 0; - } - } - - /// - /// Gets or sets the Port - /// - public int Port { get; set; } - - /// - /// S+ helper for Port - /// - public ushort UPort - { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } - } - - /// - /// Bool to show whether the server requires a preshared key. Must be set the same in the client, and if true shared keys must be identical on server/client - /// - public bool SharedKeyRequired { get; set; } - - /// - /// S+ helper for requires shared key bool - /// - public ushort USharedKeyRequired - { - set - { - if (value == 1) - SharedKeyRequired = true; - else - SharedKeyRequired = false; - } - } - - /// - /// Gets or sets the SharedKey - /// - public string SharedKey { get; set; } - - /// - /// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received - /// - public bool HeartbeatRequired { get; set; } - - /// - /// S+ Helper for Heartbeat Required - /// - public ushort UHeartbeatRequired - { - set - { - if (value == 1) - HeartbeatRequired = true; - else - HeartbeatRequired = false; - } - } - - /// - /// Gets or sets the HeartbeatRequiredIntervalMs - /// - public int HeartbeatRequiredIntervalMs { get; set; } - - /// - /// Simpl+ Heartbeat Analog value in seconds - /// - public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } } - - /// - /// Gets or sets the HeartbeatStringToMatch - /// - public string HeartbeatStringToMatch { get; set; } - - //private timers for Heartbeats per client - Dictionary HeartbeatTimerDictionary = new Dictionary(); - - //flags to show the secure server is waiting for client at index to send the shared key - List WaitingForSharedKey = new List(); - - List ClientReadyAfterKeyExchange = new List(); - - /// - /// The connected client indexes - /// - public List ConnectedClientsIndexes = new List(); - - /// - /// Gets or sets the BufferSize - /// - public int BufferSize { get; set; } - - /// - /// Private flag to note that the server has stopped intentionally - /// - private bool ServerStopped { get; set; } - - //Servers - SecureTCPServer SecureServer; - - /// - /// - /// - bool ProgramIsStopping; - - #endregion - - #region Constructors - /// - /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually. - /// - public GenericSecureTcpIpServer() - : base("Uninitialized Secure TCP Server") - { - HeartbeatRequiredIntervalInSeconds = 15; - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - BufferSize = 2000; - MonitorClientMaxFailureCount = 3; - } - - /// - /// constructor with debug key set at instantiation. Make sure to set all properties before listening. - /// - /// - public GenericSecureTcpIpServer(string key) - : base("Uninitialized Secure TCP Server") - { - HeartbeatRequiredIntervalInSeconds = 15; - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - BufferSize = 2000; - MonitorClientMaxFailureCount = 3; - Key = key; - } - - /// - /// Contstructor that sets all properties by calling the initialize method with a config object. This does set Queue size. - /// - /// - public GenericSecureTcpIpServer(TcpServerConfigObject serverConfigObject) - : base("Uninitialized Secure TCP Server") - { - HeartbeatRequiredIntervalInSeconds = 15; - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - BufferSize = 2000; - MonitorClientMaxFailureCount = 3; - Initialize(serverConfigObject); - } - #endregion - - #region Methods - Server Actions - /// - /// KillServer method - /// - public void KillServer() - { - ServerStopped = true; - if (MonitorClient != null) - { - MonitorClient.Disconnect(); - } - DisconnectAllClientsForShutdown(); - StopListening(); - } - - /// - /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ - /// - /// - /// - /// Initialize method - /// - public void Initialize(string key) - { - Key = key; - } - - /// - /// Initialze the server - /// - /// - public void Initialize(TcpServerConfigObject serverConfigObject) - { - try - { - if (serverConfigObject != null || string.IsNullOrEmpty(serverConfigObject.Key)) - { - Key = serverConfigObject.Key; - MaxClients = serverConfigObject.MaxClients; - Port = serverConfigObject.Port; - SharedKeyRequired = serverConfigObject.SharedKeyRequired; - SharedKey = serverConfigObject.SharedKey; - HeartbeatRequired = serverConfigObject.HeartbeatRequired; - HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds; - HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch; - BufferSize = serverConfigObject.BufferSize; - ReceiveQueueSize = serverConfigObject.ReceiveQueueSize > 20 ? serverConfigObject.ReceiveQueueSize : 20; - MessageQueue = new CrestronQueue(ReceiveQueueSize); - } - else - { - ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); - } - } - catch + else { ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); } } - - /// - /// Listen method - /// - public void Listen() + catch { - ServerCCSection.Enter(); - try + ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); + } + } + + /// + /// Start listening on the specified port + /// + public void Listen() + { + ServerCCSection.Enter(); + try + { + if (Port < 1 || Port > 65535) { - if (Port < 1 || Port > 65535) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); - ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); - return; - } - if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); - ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); - return; - } + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); + ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); + ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); + return; + } if (SecureServer == null) @@ -446,19 +444,19 @@ namespace PepperDash.Core Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus); ServerCCSection.Leave(); - } - catch (Exception ex) - { - ServerCCSection.Leave(); - ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key); - } } - - /// - /// StopListening method - /// - public void StopListening() + catch (Exception ex) { + ServerCCSection.Leave(); + ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key); + } + } + + /// + /// Stop Listeneing + /// + public void StopListening() + { try { Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); @@ -474,153 +472,129 @@ namespace PepperDash.Core { Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); } - } + } - /// - /// Disconnects Client - /// - /// - /// - /// DisconnectClient method - /// - public void DisconnectClient(uint client) + /// + /// Disconnects Client + /// + /// + public void DisconnectClient(uint client) + { + try { - try - { - SecureServer.Disconnect(client); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); - } - catch (Exception ex) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); - } + SecureServer.Disconnect(client); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); } - /// - /// DisconnectAllClientsForShutdown method - /// - public void DisconnectAllClientsForShutdown() + catch (Exception ex) { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); - if (SecureServer != null) + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); + } + } + /// + /// Disconnect All Clients + /// + public void DisconnectAllClientsForShutdown() + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); + if (SecureServer != null) + { + SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange; + foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly { - SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange; - foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly + var i = index; + if (!SecureServer.ClientConnected(index)) + continue; + try { - var i = index; - if (!SecureServer.ClientConnected(index)) - continue; - try - { - SecureServer.Disconnect(i); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); - } - catch (Exception ex) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); - } + SecureServer.Disconnect(i); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); } - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); - } - - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); - ConnectedClientsIndexes.Clear(); - - if (!ProgramIsStopping) - { - OnConnectionChange(); - OnServerStateChange(SecureServer.State); //State shows both listening and connected - } - - // var o = new { }; - } - - /// - /// Broadcast text from server to all connected clients - /// - /// - /// - /// BroadcastText method - /// - public void BroadcastText(string text) - { - CCriticalSection CCBroadcast = new CCriticalSection(); - CCBroadcast.Enter(); - try - { - if (ConnectedClientsIndexes.Count > 0) + catch (Exception ex) { - byte[] b = Encoding.GetEncoding(28591).GetBytes(text); - foreach (uint i in ConnectedClientsIndexes) - { - if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i))) - { - SocketErrorCodes error = SecureServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); - if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) - this.LogVerbose("{error}", error); - } - } + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); } - CCBroadcast.Leave(); - } - catch (Exception ex) - { - CCBroadcast.Leave(); - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message); } + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); } - /// - /// Not sure this is useful in library, maybe Pro?? - /// - /// - /// - /// - /// SendTextToClient method - /// - public void SendTextToClient(string text, uint clientIndex) + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); + ConnectedClientsIndexes.Clear(); + + if (!ProgramIsStopping) { - try + OnConnectionChange(); + OnServerStateChange(SecureServer.State); //State shows both listening and connected + } + + // var o = new { }; + } + + /// + /// Broadcast text from server to all connected clients + /// + /// + public void BroadcastText(string text) + { + CCriticalSection CCBroadcast = new CCriticalSection(); + CCBroadcast.Enter(); + try + { + if (ConnectedClientsIndexes.Count > 0) { byte[] b = Encoding.GetEncoding(28591).GetBytes(text); - if (SecureServer != null && SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + foreach (uint i in ConnectedClientsIndexes) { - if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) - SecureServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i))) + { + SocketErrorCodes error = SecureServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); + if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) + this.LogVerbose("{error}", error); + } } } - catch (Exception ex) + CCBroadcast.Leave(); + } + catch (Exception ex) + { + CCBroadcast.Leave(); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message); + } + } + + /// + /// Not sure this is useful in library, maybe Pro?? + /// + /// + /// + public void SendTextToClient(string text, uint clientIndex) + { + try + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(text); + if (SecureServer != null && SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) { - Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) + SecureServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); } } - - //private method to check heartbeat requirements and start or reset timer - string checkHeartbeat(uint clientIndex, string received) + catch (Exception ex) { - try + Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + } + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(uint clientIndex, string received) + { + try + { + if (HeartbeatRequired) { - if (HeartbeatRequired) + if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) { - if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) - { - var remainingText = received.Replace(HeartbeatStringToMatch, ""); - var noDelimiter = received.Trim(new char[] { '\r', '\n' }); - if (noDelimiter.Contains(HeartbeatStringToMatch)) - { - if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) - HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); - else - { - CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); - HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); - } - Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); - // Return Heartbeat - SendTextToClient(HeartbeatStringToMatch, clientIndex); - return remainingText; - } - } - else + var remainingText = received.Replace(HeartbeatStringToMatch, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatStringToMatch)) { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); @@ -629,182 +603,194 @@ namespace PepperDash.Core CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } - Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); + Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); + // Return Heartbeat + SendTextToClient(HeartbeatStringToMatch, clientIndex); + return remainingText; } } - } - catch (Exception ex) - { - Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); - } - return received; - } - - /// - /// Get the IP Address for the client at the specifed index - /// - /// - /// - /// - /// GetClientIPAddress method - /// - public string GetClientIPAddress(uint clientIndex) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); - if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) - { - var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); - return ipa; - - } - else - { - return ""; + else + { + if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) + HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); + else + { + CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); + HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + } + Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); + } } } - - #endregion - - #region Methods - HeartbeatTimer Callback - - void HeartbeatTimer_CallbackFunction(object o) + catch (Exception ex) { - uint clientIndex = 99999; - string address = string.Empty; - try + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + } + return received; + } + + /// + /// Get the IP Address for the client at the specifed index + /// + /// + /// + public string GetClientIPAddress(uint clientIndex) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) + { + var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); + return ipa; + + } + else + { + return ""; + } + } + + #endregion + + #region Methods - HeartbeatTimer Callback + + void HeartbeatTimer_CallbackFunction(object o) + { + uint clientIndex = 99999; + string address = string.Empty; + try + { + clientIndex = (uint)o; + address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", + address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); + + if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); + + var discoResult = SecureServer.Disconnect(clientIndex); + //Debug.Console(1, this, "{0}", discoResult); + + if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) { - clientIndex = (uint)o; - address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Dispose(); + HeartbeatTimerDictionary.Remove(clientIndex); + } + } + catch (Exception ex) + { + ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key); + } + } - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", - address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); + #endregion - if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) - SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); + #region Methods - Socket Status Changed Callbacks + /// + /// Secure Server Socket Status Changed Callback + /// + /// + /// + /// + void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus) + { + try + { + - var discoResult = SecureServer.Disconnect(clientIndex); - //Debug.Console(1, this, "{0}", discoResult); - - if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) + // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber); + + if (ConnectedClientsIndexes.Contains(clientIndex)) + ConnectedClientsIndexes.Remove(clientIndex); + if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) { HeartbeatTimerDictionary[clientIndex].Stop(); HeartbeatTimerDictionary[clientIndex].Dispose(); HeartbeatTimerDictionary.Remove(clientIndex); } - } - catch (Exception ex) - { - ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key); - } - } - - #endregion - - #region Methods - Socket Status Changed Callbacks - /// - /// Secure Server Socket Status Changed Callback - /// - /// - /// - /// - void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus) - { - try - { - - - // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); - if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber); - - if (ConnectedClientsIndexes.Contains(clientIndex)) - ConnectedClientsIndexes.Remove(clientIndex); - if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) - { - HeartbeatTimerDictionary[clientIndex].Stop(); - HeartbeatTimerDictionary[clientIndex].Dispose(); - HeartbeatTimerDictionary.Remove(clientIndex); - } - if (ClientReadyAfterKeyExchange.Contains(clientIndex)) - ClientReadyAfterKeyExchange.Remove(clientIndex); + if (ClientReadyAfterKeyExchange.Contains(clientIndex)) + ClientReadyAfterKeyExchange.Remove(clientIndex); if (WaitingForSharedKey.Contains(clientIndex)) WaitingForSharedKey.Remove(clientIndex); if (SecureServer.MaxNumberOfClientSupported > SecureServer.NumberOfClientsConnected) { Listen(); } - } } - catch (Exception ex) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); - } - //Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state - //after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag - //is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event. - CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null); } - - #endregion - - #region Methods Connected Callbacks - /// - /// Secure TCP Client Connected to Secure Server Callback - /// - /// - /// - void SecureConnectCallback(SecureTCPServer server, uint clientIndex) + catch (Exception ex) { - try + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); + } + //Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state + //after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag + //is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event. + CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null); + } + + #endregion + + #region Methods Connected Callbacks + /// + /// Secure TCP Client Connected to Secure Server Callback + /// + /// + /// + void SecureConnectCallback(SecureTCPServer server, uint clientIndex) + { + try + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", + server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), + clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); + if (clientIndex != 0) { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", - server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), - clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); - if (clientIndex != 0) + if (server.ClientConnected(clientIndex)) { - if (server.ClientConnected(clientIndex)) + + if (!ConnectedClientsIndexes.Contains(clientIndex)) { - - if (!ConnectedClientsIndexes.Contains(clientIndex)) - { - ConnectedClientsIndexes.Add(clientIndex); - } - if (SharedKeyRequired) - { - if (!WaitingForSharedKey.Contains(clientIndex)) - { - WaitingForSharedKey.Add(clientIndex); - } - byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); - server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); - } - else - { - OnServerClientReadyForCommunications(clientIndex); - } - if (HeartbeatRequired) - { - if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) - { - HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); - } - } - - server.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); + ConnectedClientsIndexes.Add(clientIndex); } - } - else - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); + if (SharedKeyRequired) + { + if (!WaitingForSharedKey.Contains(clientIndex)) + { + WaitingForSharedKey.Add(clientIndex); + } + byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); + server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + } + else + { + OnServerClientReadyForCommunications(clientIndex); + } + if (HeartbeatRequired) + { + if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) + { + HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); + } + } + + server.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); } } - catch (Exception ex) + else { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); } + } + catch (Exception ex) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + } // Rearm the listner SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); @@ -820,279 +806,278 @@ namespace PepperDash.Core } } - #endregion + #endregion - #region Methods - Send/Receive Callbacks - /// - /// Secure Received Data Async Callback - /// - /// - /// - /// - void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived) + #region Methods - Send/Receive Callbacks + /// + /// Secure Received Data Async Callback + /// + /// + /// + /// + void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived) + { + if (numberOfBytesReceived > 0) { - if (numberOfBytesReceived > 0) - { - string received = "Nothing"; - var handler = TextReceivedQueueInvoke; - try + string received = "Nothing"; + var handler = TextReceivedQueueInvoke; + try + { + byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); + received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived); + if (WaitingForSharedKey.Contains(clientIndex)) { - byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); - received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived); - if (WaitingForSharedKey.Contains(clientIndex)) + received = received.Replace("\r", ""); + received = received.Replace("\n", ""); + if (received != SharedKey) { - received = received.Replace("\r", ""); - received = received.Replace("\n", ""); - if (received != SharedKey) - { - byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); - mySecureTCPServer.SendData(clientIndex, b, b.Length); - mySecureTCPServer.Disconnect(clientIndex); - - return; - } + byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); + mySecureTCPServer.SendData(clientIndex, b, b.Length); + mySecureTCPServer.Disconnect(clientIndex); + + return; + } - WaitingForSharedKey.Remove(clientIndex); - byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); - mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); - OnServerClientReadyForCommunications(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); - } - else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) - { - onTextReceived(received, clientIndex); - if (handler != null) - { - MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(received, clientIndex)); - } - } + WaitingForSharedKey.Remove(clientIndex); + byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); + mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); + OnServerClientReadyForCommunications(clientIndex); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); } - catch (Exception ex) + else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); + onTextReceived(received, clientIndex); + if (handler != null) + { + MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(received, clientIndex)); + } } + } + catch (Exception ex) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); + } if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); - //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. - if (handler != null) - { - var gotLock = DequeueLock.TryEnter(); - if (gotLock) - CrestronInvoke.BeginInvoke((o) => DequeueEvent()); - } + //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. + if (handler != null) + { + var gotLock = DequeueLock.TryEnter(); + if (gotLock) + CrestronInvoke.BeginInvoke((o) => DequeueEvent()); } + } else { mySecureTCPServer.Disconnect(clientIndex); - } } + } - /// - /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. - /// It will dequeue items as they are enqueued automatically. - /// - void DequeueEvent() + /// + /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// It will dequeue items as they are enqueued automatically. + /// + void DequeueEvent() + { + try { - try + while (true) { - while (true) - { - // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. - var message = MessageQueue.Dequeue(); - var handler = TextReceivedQueueInvoke; - if (handler != null) - { - handler(this, message); - } - } - } - catch (Exception e) - { - this.LogException(e, "DequeueEvent error"); - } - // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. - if (DequeueLock != null) - { - DequeueLock.Leave(); - } - } - - #endregion - - #region Methods - EventHelpers/Callbacks - - //Private Helper method to call the Connection Change Event - void onConnectionChange(uint clientIndex, SocketStatus clientStatus) - { - if (clientIndex != 0) //0 is error not valid client change - { - var handler = ClientConnectionChange; + // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. + var message = MessageQueue.Dequeue(); + var handler = TextReceivedQueueInvoke; if (handler != null) { - handler(this, new GenericTcpServerSocketStatusChangeEventArgs(SecureServer, clientIndex, clientStatus)); + handler(this, message); } } } - - //Private Helper method to call the Connection Change Event - void OnConnectionChange() + catch (Exception e) + { + this.LogError(e, "DequeueEvent error"); + } + // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. + if (DequeueLock != null) + { + DequeueLock.Leave(); + } + } + + #endregion + + #region Methods - EventHelpers/Callbacks + + //Private Helper method to call the Connection Change Event + void onConnectionChange(uint clientIndex, SocketStatus clientStatus) + { + if (clientIndex != 0) //0 is error not valid client change { - if (ProgramIsStopping) - { - return; - } var handler = ClientConnectionChange; if (handler != null) { - handler(this, new GenericTcpServerSocketStatusChangeEventArgs()); + handler(this, new GenericTcpServerSocketStatusChangeEventArgs(SecureServer, clientIndex, clientStatus)); } } + } - //Private Helper Method to call the Text Received Event - void onTextReceived(string text, uint clientIndex) + //Private Helper method to call the Connection Change Event + void OnConnectionChange() + { + if (ProgramIsStopping) { - var handler = TextReceived; - if (handler != null) - handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex)); + return; } - - //Private Helper Method to call the Server State Change Event - void OnServerStateChange(ServerState state) + var handler = ClientConnectionChange; + if (handler != null) { - if (ProgramIsStopping) - { - return; - } - var handler = ServerStateChange; - if (handler != null) - { - handler(this, new GenericTcpServerStateChangedEventArgs(state)); - } + handler(this, new GenericTcpServerSocketStatusChangeEventArgs()); } + } - /// - /// Private Event Handler method to handle the closing of connections when the program stops - /// - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + //Private Helper Method to call the Text Received Event + void onTextReceived(string text, uint clientIndex) + { + var handler = TextReceived; + if (handler != null) + handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex)); + } + + //Private Helper Method to call the Server State Change Event + void OnServerStateChange(ServerState state) + { + if (ProgramIsStopping) { - if (programEventType == eProgramStatusEventType.Stopping) - { - ProgramIsStopping = true; - // kill bandaid things - if (MonitorClientTimer != null) - MonitorClientTimer.Stop(); - if (MonitorClient != null) - MonitorClient.Disconnect(); - - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); - KillServer(); - } + return; } - - //Private event handler method to raise the event that the server is ready to send data after a successful client shared key negotiation - void OnServerClientReadyForCommunications(uint clientIndex) + var handler = ServerStateChange; + if (handler != null) { - ClientReadyAfterKeyExchange.Add(clientIndex); - var handler = ServerClientReadyForCommunications; - if (handler != null) - handler(this, new GenericTcpServerSocketStatusChangeEventArgs( - this, clientIndex, SecureServer.GetServerSocketStatusForSpecificClient(clientIndex))); + handler(this, new GenericTcpServerStateChangedEventArgs(state)); } - #endregion + } - #region Monitor Client - /// - /// Starts the monitor client cycle. Timed wait, then call RunMonitorClient - /// - void StartMonitorClient() + /// + /// Private Event Handler method to handle the closing of connections when the program stops + /// + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) { + ProgramIsStopping = true; + // kill bandaid things if (MonitorClientTimer != null) - { - return; - } - MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); - } - - /// - /// - /// - void RunMonitorClient() - { - MonitorClient = new GenericSecureTcpIpClient_ForServer(Key + "-MONITOR", "127.0.0.1", Port, 2000); - MonitorClient.SharedKeyRequired = this.SharedKeyRequired; - MonitorClient.SharedKey = this.SharedKey; - MonitorClient.ConnectionHasHungCallback = MonitorClientHasHungCallback; - //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; - MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; - - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); - - MonitorClient.Connect(); - // From here MonitorCLient either connects or hangs, MonitorClient will call back - - } - - /// - /// - /// - void StopMonitorClient() - { - if (MonitorClient == null) - return; - - MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm; - MonitorClient.Disconnect(); - MonitorClient = null; - } - - /// - /// On monitor connect, restart the operation - /// - void MonitorClient_IsReadyForComm(object sender, GenericTcpServerClientReadyForcommunicationsEventArgs args) - { - if (args.IsReady) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); MonitorClientTimer.Stop(); - MonitorClientTimer = null; - MonitorClientFailureCount = 0; - CrestronEnvironment.Sleep(2000); - StopMonitorClient(); - StartMonitorClient(); - } - } + if (MonitorClient != null) + MonitorClient.Disconnect(); - /// - /// If the client hangs, add to counter and maybe fire the choke event - /// - void MonitorClientHasHungCallback() + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); + KillServer(); + } + } + + //Private event handler method to raise the event that the server is ready to send data after a successful client shared key negotiation + void OnServerClientReadyForCommunications(uint clientIndex) + { + ClientReadyAfterKeyExchange.Add(clientIndex); + var handler = ServerClientReadyForCommunications; + if (handler != null) + handler(this, new GenericTcpServerSocketStatusChangeEventArgs( + this, clientIndex, SecureServer.GetServerSocketStatusForSpecificClient(clientIndex))); + } + #endregion + + #region Monitor Client + /// + /// Starts the monitor client cycle. Timed wait, then call RunMonitorClient + /// + void StartMonitorClient() + { + if (MonitorClientTimer != null) { - MonitorClientFailureCount++; + return; + } + MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); + } + + /// + /// + /// + void RunMonitorClient() + { + MonitorClient = new GenericSecureTcpIpClient_ForServer(Key + "-MONITOR", "127.0.0.1", Port, 2000); + MonitorClient.SharedKeyRequired = this.SharedKeyRequired; + MonitorClient.SharedKey = this.SharedKey; + MonitorClient.ConnectionHasHungCallback = MonitorClientHasHungCallback; + //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; + MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); + + MonitorClient.Connect(); + // From here MonitorCLient either connects or hangs, MonitorClient will call back + + } + + /// + /// + /// + void StopMonitorClient() + { + if (MonitorClient == null) + return; + + MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm; + MonitorClient.Disconnect(); + MonitorClient = null; + } + + /// + /// On monitor connect, restart the operation + /// + void MonitorClient_IsReadyForComm(object sender, GenericTcpServerClientReadyForcommunicationsEventArgs args) + { + if (args.IsReady) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); MonitorClientTimer.Stop(); MonitorClientTimer = null; + MonitorClientFailureCount = 0; + CrestronEnvironment.Sleep(2000); StopMonitorClient(); - if (MonitorClientFailureCount < MonitorClientMaxFailureCount) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", - MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); - StartMonitorClient(); - } - else - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, - "\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************", - MonitorClientMaxFailureCount); - - var handler = ServerHasChoked; - if (handler != null) - handler(); - // Some external thing is in charge here. Expected reset of program - } + StartMonitorClient(); } - #endregion } + + /// + /// If the client hangs, add to counter and maybe fire the choke event + /// + void MonitorClientHasHungCallback() + { + MonitorClientFailureCount++; + MonitorClientTimer.Stop(); + MonitorClientTimer = null; + StopMonitorClient(); + if (MonitorClientFailureCount < MonitorClientMaxFailureCount) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", + MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); + StartMonitorClient(); + } + else + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, + "\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************", + MonitorClientMaxFailureCount); + + var handler = ServerHasChoked; + if (handler != null) + handler(); + // Some external thing is in charge here. Expected reset of program + } + } + #endregion } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index df44ab51..edc89a84 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -9,658 +9,562 @@ using PepperDash.Core.Logging; using Renci.SshNet; using Renci.SshNet.Common; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// +/// +public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect { + private const string SPlusKey = "Uninitialized SshClient"; + /// + /// Object to enable stream debugging + /// + public CommunicationStreamDebugging StreamDebugging { get; private set; } + + /// + /// Event that fires when data is received. Delivers args with byte array + /// + public event EventHandler BytesReceived; + + /// + /// Event that fires when data is received. Delivered as text. + /// + public event EventHandler TextReceived; + + /// + /// Event when the connection status changes. + /// + public event EventHandler ConnectionChange; + + ///// + ///// + ///// + //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; + + /// + /// Address of server + /// + public string Hostname { get; set; } + + /// + /// Port on server + /// + public int Port { get; set; } + + /// + /// Username for server + /// + public string Username { get; set; } + + /// + /// And... Password for server. That was worth documenting! + /// + public string Password { get; set; } + + /// + /// True when the server is connected - when status == 2. + /// + public bool IsConnected + { + // returns false if no client or not connected + get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + /// /// SSH Client /// - public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect + public SocketStatus ClientStatus { - private const string SPlusKey = "Uninitialized SshClient"; - - /// - /// Object to enable stream debugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } - - /// - /// Event that fires when data is received. Delivers args with byte array - /// - public event EventHandler BytesReceived; - - /// - /// Event that fires when data is received. Delivered as text. - /// - public event EventHandler TextReceived; - - /// - /// Event when the connection status changes. - /// - public event EventHandler ConnectionChange; - - /// - /// Gets or sets the Hostname - /// - public string Hostname { get; set; } - - /// - /// Port on server - /// - public int Port { get; set; } - - /// - /// Gets or sets the Username - /// - public string Username { get; set; } - - /// - /// Gets or sets the Password - /// - public string Password { get; set; } - - /// - /// True when the server is connected - when status == 2. - /// - public bool IsConnected + get { return _ClientStatus; } + private set { - // returns false if no client or not connected - get { return client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + if (_ClientStatus == value) + return; + _ClientStatus = value; + OnConnectionChange(); } + } + SocketStatus _ClientStatus; - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected - { - get { return (ushort)(IsConnected ? 1 : 0); } - } + /// + /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event + /// and IsConnected with be true when this == 2. + /// + public ushort UStatus + { + get { return (ushort)_ClientStatus; } + } - /// - /// Socket status change event - /// - public SocketStatus ClientStatus - { - get { lock (_stateLock) { return _ClientStatus; } } - private set - { - bool shouldFireEvent = false; - lock (_stateLock) + /// + /// Determines whether client will attempt reconnection on failure. Default is true + /// + public bool AutoReconnect { get; set; } + + /// + /// Will be set and unset by connect and disconnect only + /// + public bool ConnectEnabled { get; private set; } + + /// + /// S+ helper for AutoReconnect + /// + public ushort UAutoReconnect + { + get { return (ushort)(AutoReconnect ? 1 : 0); } + set { AutoReconnect = value == 1; } + } + + /// + /// Millisecond value, determines the timeout period in between reconnect attempts. + /// Set to 5000 by default + /// + public int AutoReconnectIntervalMs { get; set; } + + SshClient Client; + + ShellStream TheStream; + + CTimer ReconnectTimer; + + //Lock object to prevent simulatneous connect/disconnect operations + //private CCriticalSection connectLock = new CCriticalSection(); + private SemaphoreSlim connectLock = new SemaphoreSlim(1); + + private bool DisconnectLogged = false; + + /// + /// Typical constructor. + /// + public GenericSshClient(string key, string hostname, int port, string username, string password) : + base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Key = key; + Hostname = hostname; + Port = port; + Username = username; + Password = password; + AutoReconnectIntervalMs = 5000; + + ReconnectTimer = new CTimer(o => { - if (_ClientStatus != value) - { - _ClientStatus = value; - shouldFireEvent = true; - } - } - // Fire event outside lock to avoid deadlock - if (shouldFireEvent) - OnConnectionChange(); - } - } - - private SocketStatus _ClientStatus; - private bool _ConnectEnabled; - - /// - /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event - /// and IsConnected with be true when this == 2. - /// - public ushort UStatus - { - get { lock (_stateLock) { return (ushort)_ClientStatus; } } - } - - /// - /// Determines whether client will attempt reconnection on failure. Default is true - /// - public bool AutoReconnect { get; set; } - - /// - /// Will be set and unset by connect and disconnect only - /// - public bool ConnectEnabled - { - get { lock (_stateLock) { return _ConnectEnabled; } } - private set { lock (_stateLock) { _ConnectEnabled = value; } } - } - - /// - /// S+ helper for AutoReconnect - /// - public ushort UAutoReconnect - { - get { return (ushort)(AutoReconnect ? 1 : 0); } - set { AutoReconnect = value == 1; } - } - - /// - /// Gets or sets the AutoReconnectIntervalMs - /// - public int AutoReconnectIntervalMs { get; set; } - - private SshClient client; - - private ShellStream shellStream; - - private readonly Timer reconnectTimer; - - //Lock object to prevent simulatneous connect/disconnect operations - //private CCriticalSection connectLock = new CCriticalSection(); - private readonly SemaphoreSlim connectLock = new SemaphoreSlim(1); - - // 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. - /// - public GenericSshClient(string key, string hostname, int port, string username, string password) : - base(key) - { - StreamDebugging = new CommunicationStreamDebugging(key); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - Key = key; - Hostname = hostname; - Port = port; - Username = username; - Password = password; - AutoReconnectIntervalMs = 5000; - - reconnectTimer = new Timer(o => - { - if (ConnectEnabled) // Now thread-safe property access + if (ConnectEnabled) { Connect(); } - }, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + }, System.Threading.Timeout.Infinite); + } + + /// + /// S+ Constructor - Must set all properties before calling Connect + /// + public GenericSshClient() + : base(SPlusKey) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + + ReconnectTimer = new CTimer(o => + { + if (ConnectEnabled) + { + Connect(); + } + }, System.Threading.Timeout.Infinite); + } + + /// + /// Handles closing this up when the program shuts down + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + { + if (Client != null) + { + this.LogDebug("Program stopping. Closing connection"); + Disconnect(); + } + } + } + + /// + /// Connect to the server, using the provided properties. + /// + public void Connect() + { + // Don't go unless everything is here + if (string.IsNullOrEmpty(Hostname) || Port < 1 || Port > 65535 + || Username == null || Password == null) + { + this.LogError("Connect failed. Check hostname, port, username and password are set or not null"); + return; } - /// - /// S+ Constructor - Must set all properties before calling Connect - /// - public GenericSshClient() - : base(SPlusKey) - { - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; + ConnectEnabled = true; - reconnectTimer = new Timer(o => + try + { + connectLock.Wait(); + if (IsConnected) { - if (ConnectEnabled) // Now thread-safe property access + this.LogDebug("Connection already connected. Exiting Connect"); + } + else + { + this.LogDebug("Attempting connect"); + + // Cancel reconnect if running. + if (ReconnectTimer != null) { - Connect(); + ReconnectTimer.Stop(); } - }, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); - } - /// - /// Handles closing this up when the program shuts down - /// - private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping) - { - if (client != null) + // Cleanup the old client if it already exists + if (Client != null) { - this.LogDebug("Program stopping. Closing connection"); - Disconnect(); + this.LogDebug("Cleaning up disconnected client"); + KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); + } + + // This handles both password and keyboard-interactive (like on OS-X, 'nixes) + KeyboardInteractiveAuthenticationMethod kauth = new KeyboardInteractiveAuthenticationMethod(Username); + kauth.AuthenticationPrompt += new EventHandler(kauth_AuthenticationPrompt); + PasswordAuthenticationMethod pauth = new PasswordAuthenticationMethod(Username, Password); + + this.LogDebug("Creating new SshClient"); + ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); + 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) + { + // empty the buffer if there is data + string str = TheStream.Read(); + } + TheStream.DataReceived += Stream_DataReceived; + this.LogInformation("Connected"); + ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; + DisconnectLogged = false; + } + catch (SshConnectionException e) + { + var ie = e.InnerException; // The details are inside!! + var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; + + if (ie is SocketException) + { + this.LogException(ie, "CONNECTION failure: Cannot reach host"); + } + + if (ie is System.Net.Sockets.SocketException socketException) + { + this.LogException(ie, "Connection failure: Cannot reach {host} on {port}", + Hostname, Port); + } + if (ie is SshAuthenticationException) + { + this.LogException(ie, "Authentication failure for username {userName}", Username); + } + else + this.LogException(ie, "Error on connect"); + + DisconnectLogged = true; + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + if (AutoReconnect) + { + this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); + ReconnectTimer.Reset(AutoReconnectIntervalMs); + } + } + catch (SshOperationTimeoutException ex) + { + this.LogWarning("Connection attempt timed out: {message}", ex.Message); + + DisconnectLogged = true; + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + if (AutoReconnect) + { + this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); + ReconnectTimer.Reset(AutoReconnectIntervalMs); + } + } + catch (Exception e) + { + var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; + this.LogException(e, "Unhandled exception on connect"); + DisconnectLogged = true; + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + if (AutoReconnect) + { + this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); + ReconnectTimer.Reset(AutoReconnectIntervalMs); + } } } } - - /// - /// Connect method - /// - public void Connect() + finally { - // Don't go unless everything is here - if (string.IsNullOrEmpty(Hostname) || Port < 1 || Port > 65535 - || Username == null || Password == null) + connectLock.Release(); + } + } + + /// + /// Disconnect method + /// + public void Disconnect() + { + ConnectEnabled = false; + // Stop trying reconnects, if we are + ReconnectTimer.Stop(); + + KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); + } + + /// + /// Kills the stream, cleans up the client and sets it to null + /// + private void KillClient(SocketStatus status) + { + KillStream(); + + try + { + if (Client != null) { - this.LogError("Connect failed. Check hostname, port, username and password are set or not null"); - return; + Client.ErrorOccurred -= Client_ErrorOccurred; + Client.Disconnect(); + Client.Dispose(); + Client = null; + ClientStatus = status; + this.LogDebug("Disconnected"); } + } + catch (Exception ex) + { + this.LogException(ex, "Exception in Kill Client"); + } + } - ConnectEnabled = true; + /// + /// Kills the stream + /// + void KillStream() + { + try + { + if (TheStream != null) + { + TheStream.DataReceived -= Stream_DataReceived; + TheStream.Close(); + TheStream.Dispose(); + TheStream = null; + this.LogDebug("Disconnected stream"); + } + } + catch (Exception ex) + { + this.LogException(ex, "Exception in Kill Stream:{0}"); + } + } + /// + /// Handles the keyboard interactive authentication, should it be required. + /// + void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) + { + foreach (AuthenticationPrompt prompt in e.Prompts) + if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) + prompt.Response = Password; + } + + /// + /// Handler for data receive on ShellStream. Passes data across to queue for line parsing. + /// + void Stream_DataReceived(object sender, ShellDataEventArgs e) + { + if (((ShellStream)sender).Length <= 0L) + { + return; + } + var response = ((ShellStream)sender).Read(); + + var bytesHandler = BytesReceived; + + if (bytesHandler != null) + { + var bytes = Encoding.UTF8.GetBytes(response); + if (StreamDebugging.RxStreamDebuggingIsEnabled) + { + this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); + } + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + } + + var textHandler = TextReceived; + if (textHandler != null) + { + if (StreamDebugging.RxStreamDebuggingIsEnabled) + this.LogInformation("Received: '{0}'", ComTextHelper.GetDebugText(response)); + + textHandler(this, new GenericCommMethodReceiveTextArgs(response)); + } + + } + + + /// + /// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange + /// event + /// + void Client_ErrorOccurred(object sender, ExceptionEventArgs e) + { + CrestronInvoke.BeginInvoke(o => + { + if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) + this.LogError("Disconnected by remote"); + else + this.LogException(e.Exception, "Unhandled SSH client error"); try { connectLock.Wait(); - if (IsConnected) - { - this.LogDebug("Connection already connected. Exiting Connect"); - } - else - { - this.LogDebug("Attempting connect"); - - // Cancel reconnect if running. - StopReconnectTimer(); - - // Cleanup the old client if it already exists - if (client != null) - { - this.LogDebug("Cleaning up disconnected client"); - KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); - } - - // This handles both password and keyboard-interactive (like on OS-X, 'nixes) - KeyboardInteractiveAuthenticationMethod kauth = new KeyboardInteractiveAuthenticationMethod(Username); - kauth.AuthenticationPrompt += new EventHandler(kauth_AuthenticationPrompt); - PasswordAuthenticationMethod pauth = new PasswordAuthenticationMethod(Username, Password); - - this.LogDebug("Creating new SshClient"); - ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); - client = new SshClient(connectionInfo); - client.ErrorOccurred += Client_ErrorOccurred; - - //Attempt to connect - ClientStatus = SocketStatus.SOCKET_STATUS_WAITING; - try - { - 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 - shellStream.Read(); - } - shellStream.DataReceived += Stream_DataReceived; - this.LogInformation("Connected"); - ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; - disconnectLogged = false; - } - catch (SshConnectionException e) - { - var ie = e.InnerException; // The details are inside!! - - if (ie is SocketException) - { - this.LogError("CONNECTION failure: Cannot reach host"); - this.LogVerbose(ie, "Exception details: "); - } - - if (ie is System.Net.Sockets.SocketException socketException) - { - this.LogError("Connection failure: Cannot reach {host} on {port}", - Hostname, Port); - this.LogVerbose(socketException, "SocketException details: "); - } - if (ie is SshAuthenticationException) - { - this.LogError("Authentication failure for username {userName}", Username); - this.LogVerbose(ie, "AuthenticationException details: "); - } - else - { - this.LogError("Error on connect: {error}", ie.Message); - this.LogVerbose(ie, "Exception details: "); - } - - disconnectLogged = true; - KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - if (AutoReconnect) - { - this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); - StartReconnectTimer(); - } - } - catch (SshOperationTimeoutException ex) - { - this.LogWarning("Connection attempt timed out: {message}", ex.Message); - - disconnectLogged = true; - KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - if (AutoReconnect) - { - this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - StartReconnectTimer(); - } - } - catch (Exception e) - { - this.LogError("Unhandled exception on connect: {error}", e.Message); - this.LogVerbose(e, "Exception details: "); - disconnectLogged = true; - KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - if (AutoReconnect) - { - this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - StartReconnectTimer(); - } - } - } + KillClient(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY); } finally { connectLock.Release(); } - } - - /// - /// Disconnect method - /// - public void Disconnect() - { - ConnectEnabled = false; - // Stop trying reconnects, if we are - StopReconnectTimer(); - - KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); - } - - /// - /// Kills the stream, cleans up the client and sets it to null - /// - private void KillClient(SocketStatus status) - { - KillStream(); - - try + if (AutoReconnect && ConnectEnabled) { - if (client != null) - { - client.ErrorOccurred -= Client_ErrorOccurred; - client.Disconnect(); - client.Dispose(); - client = null; - ClientStatus = status; - this.LogDebug("Disconnected"); - } + this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); + ReconnectTimer.Reset(AutoReconnectIntervalMs); } - catch (Exception ex) - { - this.LogException(ex, "Exception in Kill Client"); - } - } - - /// - /// Kills the stream - /// - private void KillStream() - { - try - { - if (shellStream != null) - { - shellStream.DataReceived -= Stream_DataReceived; - shellStream.Close(); - shellStream.Dispose(); - shellStream = null; - this.LogDebug("Disconnected stream"); - } - } - catch (Exception ex) - { - this.LogException(ex, "Exception in Kill Stream:{0}"); - } - } - - /// - /// Handles the keyboard interactive authentication, should it be required. - /// - private void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) - { - foreach (AuthenticationPrompt prompt in e.Prompts) - if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) - prompt.Response = Password; - } - - /// - /// Handler for data receive on ShellStream. Passes data across to queue for line parsing. - /// - private void Stream_DataReceived(object sender, ShellDataEventArgs e) - { - if (((ShellStream)sender).Length <= 0L) - { - return; - } - var response = ((ShellStream)sender).Read(); - - var bytesHandler = BytesReceived; - - if (bytesHandler != null) - { - var bytes = Encoding.UTF8.GetBytes(response); - this.PrintReceivedBytes(bytes); - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - } - - var textHandler = TextReceived; - if (textHandler != null) - { - this.PrintReceivedText(response); - - textHandler(this, new GenericCommMethodReceiveTextArgs(response)); - } - - } - - - /// - /// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange - /// event - /// - private void Client_ErrorOccurred(object sender, ExceptionEventArgs e) - { - CrestronInvoke.BeginInvoke(o => - { - if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) - this.LogError("Disconnected by remote"); - else - this.LogException(e.Exception, "Unhandled SSH client error"); - try - { - connectLock.Wait(); - KillClient(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY); - } - finally - { - connectLock.Release(); - } - if (AutoReconnect && ConnectEnabled) - { - this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - StartReconnectTimer(); - } - }); - } - - /// - /// Helper for ConnectionChange event - /// - private void OnConnectionChange() - { - ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this)); - } - - #region IBasicCommunication Members - - /// - /// Sends text to the server - /// - /// The text to send - public void SendText(string text) - { - try - { - if (client != null && shellStream != null && IsConnected) - { - this.PrintSentText(text); - - shellStream.Write(text); - shellStream.Flush(); - } - else - { - this.LogDebug("Client is null or disconnected. Cannot Send Text"); - } - } - catch (ObjectDisposedException) - { - this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); - - KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - StartReconnectTimer(); - } - catch (Exception ex) - { - this.LogException(ex, "Exception sending text: '{message}'", text); - } - } - - /// - /// Sends Bytes to the server - /// - /// The bytes to send - public void SendBytes(byte[] bytes) - { - try - { - if (client != null && shellStream != null && IsConnected) - { - this.PrintSentBytes(bytes); - - shellStream.Write(bytes, 0, bytes.Length); - shellStream.Flush(); - } - else - { - this.LogDebug("Client is null or disconnected. Cannot Send Bytes"); - } - } - catch (ObjectDisposedException ex) - { - this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); - - KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - StartReconnectTimer(); - } - catch (Exception ex) - { - this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes)); - } - } - #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; - } - } - + }); } + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + if (ConnectionChange != null) + ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); + } + + #region IBasicCommunication Members + + /// + /// Sends text to the server + /// + /// + public void SendText(string text) + { + try + { + if (Client != null && TheStream != null && IsConnected) + { + if (StreamDebugging.TxStreamDebuggingIsEnabled) + this.LogInformation( + "Sending {length} characters of text: '{text}'", + text.Length, + ComTextHelper.GetDebugText(text)); + + TheStream.Write(text); + TheStream.Flush(); + } + else + { + this.LogDebug("Client is null or disconnected. Cannot Send Text"); + } + } + catch (ObjectDisposedException) + { + this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); + + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + ReconnectTimer.Reset(); + } + catch (Exception ex) + { + this.LogException(ex, "Exception sending text: '{message}'", text); + } + } + + /// + /// Sends Bytes to the server + /// + /// + public void SendBytes(byte[] bytes) + { + try + { + if (Client != null && TheStream != null && IsConnected) + { + if (StreamDebugging.TxStreamDebuggingIsEnabled) + this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + + TheStream.Write(bytes, 0, bytes.Length); + TheStream.Flush(); + } + else + { + this.LogDebug("Client is null or disconnected. Cannot Send Bytes"); + } + } + catch (ObjectDisposedException ex) + { + this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); + + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + ReconnectTimer.Reset(); + } + catch (Exception ex) + { + this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes)); + } + } + #endregion + //***************************************************************************************************** //***************************************************************************************************** /// - /// Represents a SshConnectionChangeEventArgs + /// Fired when connection changes /// public class SshConnectionChangeEventArgs : EventArgs { /// /// Connection State /// - public bool IsConnected { get; private set; } + public bool IsConnected { get; private set; } /// - /// Gets or sets the UIsConnected + /// Connection Status represented as a ushort /// public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } } /// - /// Gets or sets the Client + /// The client /// public GenericSshClient Client { get; private set; } /// - /// Gets or sets the Status + /// Socket Status as represented by /// public ushort Status { get { return Client.UStatus; } } @@ -674,7 +578,7 @@ namespace PepperDash.Core /// /// Connection State /// The Client - public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client) + public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client) { IsConnected = isConnected; Client = client; diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs index 16963099..e880b386 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs @@ -9,18 +9,18 @@ using Crestron.SimplSharp.CrestronSockets; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using Required = NewtonsoftJson::Newtonsoft.Json.Required; -namespace PepperDash.Core -{ - /// - /// A class to handle basic TCP/IP communications with a server - /// +namespace PepperDash.Core; + +/// +/// A class to handle basic TCP/IP communications with a server +/// public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect - { - private const string SplusKey = "Uninitialized TcpIpClient"; - /// - /// Object to enable stream debugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } +{ + private const string SplusKey = "Uninitialized TcpIpClient"; + /// + /// Object to enable stream debugging + /// + public CommunicationStreamDebugging StreamDebugging { get; private set; } /// /// Fires when data is received from the server and returns it as a Byte array @@ -41,15 +41,15 @@ namespace PepperDash.Core private string _hostname; - /// - /// Address of server - /// - public string Hostname - { - get - { - return _hostname; - } + /// + /// Address of server + /// + public string Hostname + { + get + { + return _hostname; + } set { @@ -61,77 +61,77 @@ namespace PepperDash.Core } } - /// - /// Gets or sets the Port - /// - public int Port { get; set; } + /// + /// Port on server + /// + public int Port { get; set; } - /// - /// Another damn S+ helper because S+ seems to treat large port nums as signed ints - /// which screws up things - /// - public ushort UPort - { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } - } + /// + /// Another damn S+ helper because S+ seems to treat large port nums as signed ints + /// which screws up things + /// + public ushort UPort + { + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } - /// - /// Defaults to 2000 - /// - public int BufferSize { get; set; } + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } /// /// The actual client class /// private TCPClient _client; - /// - /// Bool showing if socket is connected - /// - public bool IsConnected + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// _client socket status Read only + /// + public SocketStatus ClientStatus + { + get { - get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } - } + return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; + } + } - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected - { - get { return (ushort)(IsConnected ? 1 : 0); } - } + /// + /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event + /// and IsConnected would be true when this == 2. + /// + public ushort UStatus + { + get { return (ushort)ClientStatus; } + } - /// - /// _client socket status Read only - /// - public SocketStatus ClientStatus - { - get - { - return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; - } - } + /// + /// Status text shows the message associated with socket status + /// + public string ClientStatusText { get { return ClientStatus.ToString(); } } - /// - /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event - /// and IsConnected would be true when this == 2. - /// - public ushort UStatus - { - get { return (ushort)ClientStatus; } - } - - /// - /// Status text shows the message associated with socket status - /// - public string ClientStatusText { get { return ClientStatus.ToString(); } } - - /// - /// Ushort representation of client status - /// - [Obsolete] - public ushort UClientStatus { get { return (ushort)ClientStatus; } } + /// + /// Ushort representation of client status + /// + [Obsolete] + public ushort UClientStatus { get { return (ushort)ClientStatus; } } /// /// Connection failure reason @@ -143,14 +143,14 @@ namespace PepperDash.Core /// public bool AutoReconnect { get; set; } - /// - /// S+ helper for AutoReconnect - /// - public ushort UAutoReconnect - { - get { return (ushort)(AutoReconnect ? 1 : 0); } - set { AutoReconnect = value == 1; } - } + /// + /// S+ helper for AutoReconnect + /// + public ushort UAutoReconnect + { + get { return (ushort)(AutoReconnect ? 1 : 0); } + set { AutoReconnect = value == 1; } + } /// /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 @@ -170,293 +170,297 @@ namespace PepperDash.Core get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } - //Lock object to prevent simulatneous connect/disconnect operations - private CCriticalSection connectLock = new CCriticalSection(); + //Lock object to prevent simulatneous connect/disconnect operations + private CCriticalSection connectLock = new CCriticalSection(); - // private Timer for auto reconnect - private CTimer RetryTimer; + // private Timer for auto reconnect + private CTimer RetryTimer; - /// - /// Constructor - /// - /// unique string to differentiate between instances - /// - /// - /// + /// + /// Constructor + /// + /// unique string to differentiate between instances + /// + /// + /// public GenericTcpIpClient(string key, string address, int port, int bufferSize) - : base(key) - { - StreamDebugging = new CommunicationStreamDebugging(key); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; - Hostname = address; - Port = port; - BufferSize = bufferSize; + : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + Hostname = address; + Port = port; + BufferSize = bufferSize; - RetryTimer = new CTimer(o => - { - Reconnect(); - }, Timeout.Infinite); + RetryTimer = new CTimer(o => + { + Reconnect(); + }, Timeout.Infinite); + } + + /// + /// Constructor + /// + /// + public GenericTcpIpClient(string key) + : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + + RetryTimer = new CTimer(o => + { + Reconnect(); + }, Timeout.Infinite); + } + + /// + /// Default constructor for S+ + /// + public GenericTcpIpClient() + : base(SplusKey) + { + StreamDebugging = new CommunicationStreamDebugging(SplusKey); + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + + RetryTimer = new CTimer(o => + { + Reconnect(); + }, Timeout.Infinite); + } + + /// + /// Just to help S+ set the key + /// + public void Initialize(string key) + { + Key = key; + } + + /// + /// Handles closing this up when the program shuts down + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + { + Debug.Console(1, this, "Program stopping. Closing connection"); + Deactivate(); } + } - /// - /// Constructor - /// - /// - public GenericTcpIpClient(string key) - : base(key) + /// + /// + /// + /// + public override bool Deactivate() + { + RetryTimer.Stop(); + RetryTimer.Dispose(); + if (_client != null) { - StreamDebugging = new CommunicationStreamDebugging(key); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; - BufferSize = 2000; - - RetryTimer = new CTimer(o => - { - Reconnect(); - }, Timeout.Infinite); + _client.SocketStatusChange -= this.Client_SocketStatusChange; + DisconnectClient(); } + return true; + } - /// - /// Default constructor for S+ - /// - public GenericTcpIpClient() - : base(SplusKey) + /// + /// Attempts to connect to the server + /// + public void Connect() + { + if (string.IsNullOrEmpty(Hostname)) { - StreamDebugging = new CommunicationStreamDebugging(SplusKey); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; - BufferSize = 2000; - - RetryTimer = new CTimer(o => - { - Reconnect(); - }, Timeout.Infinite); + Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); + return; } - - /// - /// Initialize method - /// - public void Initialize(string key) + if (Port < 1 || Port > 65535) { - Key = key; - } - - /// - /// Handles closing this up when the program shuts down - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping) { - Debug.Console(1, this, "Program stopping. Closing connection"); - Deactivate(); - } - } - - /// - /// - /// - /// - /// - /// Deactivate method - /// - public override bool Deactivate() - { - RetryTimer.Stop(); - RetryTimer.Dispose(); - if (_client != null) - { - _client.SocketStatusChange -= this.Client_SocketStatusChange; - DisconnectClient(); - } - return true; - } - - /// - /// Connect method - /// - public void Connect() - { - if (string.IsNullOrEmpty(Hostname)) - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); + Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key); return; } - if (Port < 1 || Port > 65535) - { - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key); - return; - } - } - - try - { - connectLock.Enter(); - if (IsConnected) - { - Debug.Console(1, this, "Connection already connected. Exiting Connect()"); - } - else - { - //Stop retry timer if running - RetryTimer.Stop(); - _client = new TCPClient(Hostname, Port, BufferSize); - _client.SocketStatusChange -= Client_SocketStatusChange; - _client.SocketStatusChange += Client_SocketStatusChange; - DisconnectCalledByUser = false; - _client.ConnectToServerAsync(ConnectToServerCallback); - } - } - finally - { - connectLock.Leave(); - } } - private void Reconnect() + try { - if (_client == null) + connectLock.Enter(); + if (IsConnected) { - return; - } - try - { - connectLock.Enter(); - if (IsConnected || DisconnectCalledByUser == true) - { - Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()"); - } - else - { - Debug.Console(1, this, "Attempting reconnect now"); - _client.ConnectToServerAsync(ConnectToServerCallback); - } - } - finally - { - connectLock.Leave(); - } - } - - /// - /// Disconnect method - /// - public void Disconnect() - { - try - { - connectLock.Enter(); - DisconnectCalledByUser = true; - - // Stop trying reconnects, if we are - RetryTimer.Stop(); - DisconnectClient(); - } - finally - { - connectLock.Leave(); - } - } - - /// - /// DisconnectClient method - /// - public void DisconnectClient() - { - if (_client != null) - { - Debug.Console(1, this, "Disconnecting client"); - if (IsConnected) - _client.DisconnectFromServer(); - } - } - - /// - /// Callback method for connection attempt - /// - /// - void ConnectToServerCallback(TCPClient c) - { - if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) - { - Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); - WaitAndTryReconnect(); + Debug.Console(1, this, "Connection already connected. Exiting Connect()"); } else { - Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); + //Stop retry timer if running + RetryTimer.Stop(); + _client = new TCPClient(Hostname, Port, BufferSize); + _client.SocketStatusChange -= Client_SocketStatusChange; + _client.SocketStatusChange += Client_SocketStatusChange; + DisconnectCalledByUser = false; + _client.ConnectToServerAsync(ConnectToServerCallback); } } + finally + { + connectLock.Leave(); + } + } - /// - /// Disconnects, waits and attemtps to connect again - /// + private void Reconnect() + { + if (_client == null) + { + return; + } + try + { + connectLock.Enter(); + if (IsConnected || DisconnectCalledByUser == true) + { + Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()"); + } + else + { + Debug.Console(1, this, "Attempting reconnect now"); + _client.ConnectToServerAsync(ConnectToServerCallback); + } + } + finally + { + connectLock.Leave(); + } + } + + /// + /// Attempts to disconnect the client + /// + public void Disconnect() + { + try + { + connectLock.Enter(); + DisconnectCalledByUser = true; + + // Stop trying reconnects, if we are + RetryTimer.Stop(); + DisconnectClient(); + } + finally + { + connectLock.Leave(); + } + } + + /// + /// Does the actual disconnect business + /// + public void DisconnectClient() + { + if (_client != null) + { + Debug.Console(1, this, "Disconnecting client"); + if (IsConnected) + _client.DisconnectFromServer(); + } + } + + /// + /// Callback method for connection attempt + /// + /// + void ConnectToServerCallback(TCPClient c) + { + if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); + WaitAndTryReconnect(); + } + else + { + Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); + } + } + + /// + /// Disconnects, waits and attemtps to connect again + /// void WaitAndTryReconnect() + { + CrestronInvoke.BeginInvoke(o => { - CrestronInvoke.BeginInvoke(o => + try { - try + connectLock.Enter(); + if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null) { - connectLock.Enter(); - if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null) - { - DisconnectClient(); - Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus); - RetryTimer.Reset(AutoReconnectIntervalMs); - } + DisconnectClient(); + Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus); + RetryTimer.Reset(AutoReconnectIntervalMs); } - finally - { - connectLock.Leave(); - } - }); - } - - /// - /// Recieves incoming data - /// - /// - /// - void Receive(TCPClient client, int numBytes) - { - if (client != null) - { - if (numBytes > 0) - { - var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); - var bytesHandler = BytesReceived; - if (bytesHandler != null) - { - this.PrintReceivedBytes(bytes); - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - } - var textHandler = TextReceived; - if (textHandler != null) - { - var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - - this.PrintReceivedText(str); - - textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - } - } - client.ReceiveDataAsync(Receive); } - } + finally + { + connectLock.Leave(); + } + }); + } - /// - /// SendText method - /// - public void SendText(string text) + /// + /// Recieves incoming data + /// + /// + /// + void Receive(TCPClient client, int numBytes) + { + if (client != null) { - 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); + if (numBytes > 0) + { + var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); + var bytesHandler = BytesReceived; + if (bytesHandler != null) + { + if (StreamDebugging.RxStreamDebuggingIsEnabled) + { + Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); + } + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + } + var textHandler = TextReceived; + if (textHandler != null) + { + 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); + } + + textHandler(this, new GenericCommMethodReceiveTextArgs(str)); + } + } + client.ReceiveDataAsync(Receive); } + } + + /// + /// General send 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); + } /// /// SendEscapedText method @@ -471,37 +475,35 @@ namespace PepperDash.Core SendText(unescapedText); } - /// - /// Sends Bytes to the server - /// - /// - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) - { - this.PrintSentBytes(bytes); - if (_client != null) - _client.SendData(bytes, bytes.Length); - } + /// + /// Sends Bytes to the server + /// + /// + 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); + } - /// - /// Socket Status Change Handler - /// - /// - /// + /// + /// Socket Status Change Handler + /// + /// + /// void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) + { + if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { - if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) - { - Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); - WaitAndTryReconnect(); - } - else - { - Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); - _client.ReceiveDataAsync(Receive); - } + Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); + WaitAndTryReconnect(); + } + else + { + Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); + _client.ReceiveDataAsync(Receive); + } var handler = ConnectionChange; if (handler != null) @@ -509,31 +511,31 @@ namespace PepperDash.Core } } +/// +/// Configuration properties for TCP/SSH Connections +/// + public class TcpSshPropertiesConfig + { /// - /// Represents a TcpSshPropertiesConfig + /// Address to connect to /// - public class TcpSshPropertiesConfig - { - /// - /// Address to connect to - /// [JsonProperty(Required = Required.Always)] - public string Address { get; set; } - - /// - /// Port to connect to - /// - [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 Address { get; set; } + + /// + /// Port to connect to + /// + [JsonProperty(Required = Required.Always)] + public int Port { get; set; } + + /// + /// Username credential + /// + public string Username { get; set; } + /// + /// Passord credential + /// + public string Password { get; set; } /// /// Defaults to 32768 @@ -556,17 +558,16 @@ namespace PepperDash.Core [JsonProperty("disableSshEcho")] public bool DisableSshEcho { get; set; } - /// - /// Default constructor - /// + /// + /// Default constructor + /// public TcpSshPropertiesConfig() - { - BufferSize = 32768; - AutoReconnect = true; - AutoReconnectIntervalMs = 5000; - Username = ""; - Password = ""; - DisableSshEcho = false; - } - } -} + { + BufferSize = 32768; + AutoReconnect = true; + AutoReconnectIntervalMs = 5000; + Username = ""; + Password = ""; + } + + } diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs index 6d4958ca..a1a0887f 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs @@ -19,757 +19,755 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Generic TCP/IP client for server +/// +public class GenericTcpIpClient_ForServer : Device, IAutoReconnect { /// - /// Generic TCP/IP client for server + /// Band aid delegate for choked server /// - public class GenericTcpIpClient_ForServer : Device, IAutoReconnect + internal delegate void ConnectionHasHungCallbackDelegate(); + + #region Events + + //public event EventHandler BytesReceived; + + /// + /// Notifies of text received + /// + public event EventHandler TextReceived; + + /// + /// Notifies of socket status change + /// + public event EventHandler ConnectionChange; + + + /// + /// This is something of a band-aid callback. If the client times out during the connection process, because the server + /// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help + /// keep a watch on the server and reset it if necessary. + /// + internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback; + + /// + /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require + /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. + /// + public event EventHandler ClientReadyForCommunications; + + #endregion + + #region Properties & Variables + + /// + /// Address of server + /// + public string Hostname { get; set; } + + /// + /// Port on server + /// + public int Port { get; set; } + + /// + /// S+ helper + /// + public ushort UPort { - /// - /// Band aid delegate for choked server - /// - internal delegate void ConnectionHasHungCallbackDelegate(); + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } - #region Events + /// + /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class + /// + public bool SharedKeyRequired { get; set; } - //public event EventHandler BytesReceived; - - /// - /// Notifies of text received - /// - public event EventHandler TextReceived; - - /// - /// Notifies of socket status change - /// - public event EventHandler ConnectionChange; - - - /// - /// This is something of a band-aid callback. If the client times out during the connection process, because the server - /// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help - /// keep a watch on the server and reset it if necessary. - /// - internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback; - - /// - /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require - /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. - /// - public event EventHandler ClientReadyForCommunications; - - #endregion - - #region Properties & Variables - - /// - /// Address of server - /// - public string Hostname { get; set; } - - /// - /// Gets or sets the Port - /// - public int Port { get; set; } - - /// - /// S+ helper - /// - public ushort UPort + /// + /// S+ helper for requires shared key bool + /// + public ushort USharedKeyRequired + { + set { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } + if (value == 1) + SharedKeyRequired = true; + else + SharedKeyRequired = false; + } + } + + /// + /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module + /// + public string SharedKey { get; set; } + + /// + /// flag to show the client is waiting for the server to send the shared key + /// + private bool WaitingForSharedKeyResponse { get; set; } + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + /// + /// Semaphore on connect method + /// + bool IsTryingToConnect; + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get + { + if (Client != null) + return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; + else + return false; + } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// Bool showing if socket is ready for communication after shared key exchange + /// + public bool IsReadyForCommunication { get; set; } + + /// + /// S+ helper for IsReadyForCommunication + /// + public ushort UIsReadyForCommunication + { + get { return (ushort)(IsReadyForCommunication ? 1 : 0); } + } + + /// + /// Client socket status Read only + /// + public SocketStatus ClientStatus + { + get + { + if (Client != null) + return Client.ClientStatus; + else + return SocketStatus.SOCKET_STATUS_NO_CONNECT; + } + } + + /// + /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event + /// and IsConnected would be true when this == 2. + /// + public ushort UStatus + { + get { return (ushort)ClientStatus; } + } + + /// + /// Status text shows the message associated with socket status + /// + public string ClientStatusText { get { return ClientStatus.ToString(); } } + + /// + /// bool to track if auto reconnect should be set on the socket + /// + public bool AutoReconnect { get; set; } + + /// + /// S+ helper for AutoReconnect + /// + public ushort UAutoReconnect + { + get { return (ushort)(AutoReconnect ? 1 : 0); } + set { AutoReconnect = value == 1; } + } + /// + /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 + /// + public int AutoReconnectIntervalMs { get; set; } + + /// + /// Flag Set only when the disconnect method is called. + /// + bool DisconnectCalledByUser; + + /// + /// private Timer for auto reconnect + /// + CTimer RetryTimer; + + + /// + /// + /// + public bool HeartbeatEnabled { get; set; } + + /// + /// + /// + public ushort UHeartbeatEnabled + { + get { return (ushort)(HeartbeatEnabled ? 1 : 0); } + set { HeartbeatEnabled = value == 1; } + } + + /// + /// + /// + public string HeartbeatString = "heartbeat"; + + /// + /// + /// + public int HeartbeatInterval = 50000; + + CTimer HeartbeatSendTimer; + CTimer HeartbeatAckTimer; + /// + /// Used to force disconnection on a dead connect attempt + /// + CTimer ConnectFailTimer; + CTimer WaitForSharedKey; + private int ConnectionCount; + /// + /// Internal secure client + /// + TCPClient Client; + + bool ProgramIsStopping; + + #endregion + + #region Constructors + + /// + /// Constructor + /// + /// + /// + /// + /// + public GenericTcpIpClient_ForServer(string key, string address, int port, int bufferSize) + : base(key) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Hostname = address; + Port = port; + BufferSize = bufferSize; + AutoReconnectIntervalMs = 5000; + + } + + /// + /// Constructor for S+ + /// + public GenericTcpIpClient_ForServer() + : base("Uninitialized DynamicTcpClient") + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + } + #endregion + + #region Methods + + /// + /// Just to help S+ set the key + /// + public void Initialize(string key) + { + Key = key; + } + + /// + /// Handles closing this up when the program shuts down + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection"); + ProgramIsStopping = true; + Disconnect(); } - /// - /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class - /// - public bool SharedKeyRequired { get; set; } + } - /// - /// S+ helper for requires shared key bool - /// - public ushort USharedKeyRequired + /// + /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name. + /// + public void Connect() + { + ConnectionCount++; + Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); + + + if (IsConnected) { - set - { - if (value == 1) - SharedKeyRequired = true; - else - SharedKeyRequired = false; - } + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); + return; } - - /// - /// Gets or sets the SharedKey - /// - public string SharedKey { get; set; } - - /// - /// flag to show the client is waiting for the server to send the shared key - /// - private bool WaitingForSharedKeyResponse { get; set; } - - /// - /// Gets or sets the BufferSize - /// - public int BufferSize { get; set; } - - /// - /// Semaphore on connect method - /// - bool IsTryingToConnect; - - /// - /// Bool showing if socket is connected - /// - public bool IsConnected + if (IsTryingToConnect) { - get - { - if (Client != null) - return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; - else - return false; - } + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); + return; } - - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected + try { - get { return (ushort)(IsConnected ? 1 : 0); } - } - - /// - /// Bool showing if socket is ready for communication after shared key exchange - /// - public bool IsReadyForCommunication { get; set; } - - /// - /// S+ helper for IsReadyForCommunication - /// - public ushort UIsReadyForCommunication - { - get { return (ushort)(IsReadyForCommunication ? 1 : 0); } - } - - /// - /// Client socket status Read only - /// - public SocketStatus ClientStatus - { - get - { - if (Client != null) - return Client.ClientStatus; - else - return SocketStatus.SOCKET_STATUS_NO_CONNECT; - } - } - - /// - /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event - /// and IsConnected would be true when this == 2. - /// - public ushort UStatus - { - get { return (ushort)ClientStatus; } - } - - /// - /// Status text shows the message associated with socket status - /// - public string ClientStatusText { get { return ClientStatus.ToString(); } } - - /// - /// bool to track if auto reconnect should be set on the socket - /// - public bool AutoReconnect { get; set; } - - /// - /// S+ helper for AutoReconnect - /// - public ushort UAutoReconnect - { - get { return (ushort)(AutoReconnect ? 1 : 0); } - set { AutoReconnect = value == 1; } - } - /// - /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 - /// - public int AutoReconnectIntervalMs { get; set; } - - /// - /// Flag Set only when the disconnect method is called. - /// - bool DisconnectCalledByUser; - - /// - /// private Timer for auto reconnect - /// - CTimer RetryTimer; - - - /// - /// - /// - public bool HeartbeatEnabled { get; set; } - - /// - /// - /// - public ushort UHeartbeatEnabled - { - get { return (ushort)(HeartbeatEnabled ? 1 : 0); } - set { HeartbeatEnabled = value == 1; } - } - - /// - /// - /// - public string HeartbeatString = "heartbeat"; - - /// - /// - /// - public int HeartbeatInterval = 50000; - - CTimer HeartbeatSendTimer; - CTimer HeartbeatAckTimer; - /// - /// Used to force disconnection on a dead connect attempt - /// - CTimer ConnectFailTimer; - CTimer WaitForSharedKey; - private int ConnectionCount; - /// - /// Internal secure client - /// - TCPClient Client; - - bool ProgramIsStopping; - - #endregion - - #region Constructors - - /// - /// Constructor - /// - /// - /// - /// - /// - public GenericTcpIpClient_ForServer(string key, string address, int port, int bufferSize) - : base(key) - { - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - Hostname = address; - Port = port; - BufferSize = bufferSize; - AutoReconnectIntervalMs = 5000; - - } - - /// - /// Constructor for S+ - /// - public GenericTcpIpClient_ForServer() - : base("Uninitialized DynamicTcpClient") - { - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; - BufferSize = 2000; - } - #endregion - - #region Methods - - /// - /// Initialize method - /// - public void Initialize(string key) - { - Key = key; - } - - /// - /// Handles closing this up when the program shuts down - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection"); - ProgramIsStopping = true; - Disconnect(); - } - - } - - /// - /// Connect method - /// - public void Connect() - { - ConnectionCount++; - Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); - - - if (IsConnected) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); - return; - } - if (IsTryingToConnect) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); - return; - } - try - { - IsTryingToConnect = true; - if (RetryTimer != null) - { - RetryTimer.Stop(); - RetryTimer = null; - } - if (string.IsNullOrEmpty(Hostname)) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); - return; - } - if (Port < 1 || Port > 65535) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); - return; - } - if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); - return; - } - - // clean up previous client - if (Client != null) - { - Cleanup(); - } - DisconnectCalledByUser = false; - - Client = new TCPClient(Hostname, Port, BufferSize); - Client.SocketStatusChange += Client_SocketStatusChange; - if(HeartbeatEnabled) - Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); - Client.AddressClientConnectedTo = Hostname; - Client.PortNumber = Port; - // SecureClient = c; - - //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); - - ConnectFailTimer = new CTimer(o => - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); - if (IsTryingToConnect) - { - IsTryingToConnect = false; - - //if (ConnectionHasHungCallback != null) - //{ - // ConnectionHasHungCallback(); - //} - //SecureClient.DisconnectFromServer(); - //CheckClosedAndTryReconnect(); - } - }, 30000); - - Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); - Client.ConnectToServerAsync(o => - { - Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); - - if (ConnectFailTimer != null) - { - ConnectFailTimer.Stop(); - } - IsTryingToConnect = false; - - if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - { - Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); - o.ReceiveDataAsync(Receive); - - if (SharedKeyRequired) - { - WaitingForSharedKeyResponse = true; - WaitForSharedKey = new CTimer(timer => - { - - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); - // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); - // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup - o.DisconnectFromServer(); - //CheckClosedAndTryReconnect(); - //OnClientReadyForcommunications(false); // Should send false event - }, 15000); - } - else - { - //CLient connected and shared key is not required so just raise the ready for communication event. if Shared key - //required this is called by the shared key being negotiated - if (IsReadyForCommunication == false) - { - OnClientReadyForcommunications(true); // Key not required - } - } - } - else - { - Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); - CheckClosedAndTryReconnect(); - } - }); - } - catch (Exception ex) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message); - IsTryingToConnect = false; - CheckClosedAndTryReconnect(); - } - } - - /// - /// Disconnect method - /// - public void Disconnect() - { - this.LogVerbose("Disconnect Called"); - - DisconnectCalledByUser = true; - if (IsConnected) - { - Client.DisconnectFromServer(); - - } + IsTryingToConnect = true; if (RetryTimer != null) { RetryTimer.Stop(); RetryTimer = null; } - Cleanup(); - } - - /// - /// Internal call to close up client. ALWAYS use this when disconnecting. - /// - void Cleanup() - { - IsTryingToConnect = false; + if (string.IsNullOrEmpty(Hostname)) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); + return; + } + if (Port < 1 || Port > 65535) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); + return; + } + // clean up previous client if (Client != null) { - //SecureClient.DisconnectFromServer(); - Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); - Client.SocketStatusChange -= Client_SocketStatusChange; - Client.Dispose(); - Client = null; - } - if (ConnectFailTimer != null) - { - ConnectFailTimer.Stop(); - ConnectFailTimer.Dispose(); - ConnectFailTimer = null; - } - } - - - /// ff - /// Called from Connect failure or Socket Status change if - /// auto reconnect and socket disconnected (Not disconnected by user) - /// - void CheckClosedAndTryReconnect() - { - if (Client != null) - { - Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); Cleanup(); } - if (!DisconnectCalledByUser && AutoReconnect) + DisconnectCalledByUser = false; + + Client = new TCPClient(Hostname, Port, BufferSize); + Client.SocketStatusChange += Client_SocketStatusChange; + if(HeartbeatEnabled) + Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); + Client.AddressClientConnectedTo = Hostname; + Client.PortNumber = Port; + // SecureClient = c; + + //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); + + ConnectFailTimer = new CTimer(o => { - var halfInterval = AutoReconnectIntervalMs / 2; - var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; - Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); - if (RetryTimer != null) + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); + if (IsTryingToConnect) { - RetryTimer.Stop(); - RetryTimer = null; + IsTryingToConnect = false; + + //if (ConnectionHasHungCallback != null) + //{ + // ConnectionHasHungCallback(); + //} + //SecureClient.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); } - RetryTimer = new CTimer(o => Connect(), rndTime); - } - } + }, 30000); - /// - /// Receive callback - /// - /// - /// - void Receive(TCPClient client, int numBytes) - { - if (numBytes > 0) + Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); + Client.ConnectToServerAsync(o => { - string str = string.Empty; + Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); - try + if (ConnectFailTimer != null) { - var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); - str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); - if (!string.IsNullOrEmpty(checkHeartbeat(str))) + ConnectFailTimer.Stop(); + } + IsTryingToConnect = false; + + if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); + o.ReceiveDataAsync(Receive); + + if (SharedKeyRequired) { - if (SharedKeyRequired && str == "SharedKey:") + WaitingForSharedKeyResponse = true; + WaitForSharedKey = new CTimer(timer => { - Debug.Console(2, this, "Server asking for shared key, sending"); - SendText(SharedKey + "\n"); - } - else if (SharedKeyRequired && str == "Shared Key Match") + + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); + // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); + // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup + o.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + //OnClientReadyForcommunications(false); // Should send false event + }, 15000); + } + else + { + //CLient connected and shared key is not required so just raise the ready for communication event. if Shared key + //required this is called by the shared key being negotiated + if (IsReadyForCommunication == false) { - StopWaitForSharedKeyTimer(); - Debug.Console(2, this, "Shared key confirmed. Ready for communication"); - OnClientReadyForcommunications(true); // Successful key exchange - } - else - { - //var bytesHandler = BytesReceived; - //if (bytesHandler != null) - // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - var textHandler = TextReceived; - if (textHandler != null) - textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str)); + OnClientReadyForcommunications(true); // Key not required } } } - catch (Exception ex) + else { - Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); + Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); + CheckClosedAndTryReconnect(); + } + }); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message); + IsTryingToConnect = false; + CheckClosedAndTryReconnect(); + } + } + + /// + /// + /// + public void Disconnect() + { + this.LogVerbose("Disconnect Called"); + + DisconnectCalledByUser = true; + if (IsConnected) + { + Client.DisconnectFromServer(); + + } + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + Cleanup(); + } + + /// + /// Internal call to close up client. ALWAYS use this when disconnecting. + /// + void Cleanup() + { + IsTryingToConnect = false; + + if (Client != null) + { + //SecureClient.DisconnectFromServer(); + Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); + Client.SocketStatusChange -= Client_SocketStatusChange; + Client.Dispose(); + Client = null; + } + if (ConnectFailTimer != null) + { + ConnectFailTimer.Stop(); + ConnectFailTimer.Dispose(); + ConnectFailTimer = null; + } + } + + + /// ff + /// Called from Connect failure or Socket Status change if + /// auto reconnect and socket disconnected (Not disconnected by user) + /// + void CheckClosedAndTryReconnect() + { + if (Client != null) + { + Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); + Cleanup(); + } + if (!DisconnectCalledByUser && AutoReconnect) + { + var halfInterval = AutoReconnectIntervalMs / 2; + var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; + Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + RetryTimer = new CTimer(o => Connect(), rndTime); + } + } + + /// + /// Receive callback + /// + /// + /// + void Receive(TCPClient client, int numBytes) + { + if (numBytes > 0) + { + string str = string.Empty; + + try + { + var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); + str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); + if (!string.IsNullOrEmpty(checkHeartbeat(str))) + { + if (SharedKeyRequired && str == "SharedKey:") + { + Debug.Console(2, this, "Server asking for shared key, sending"); + SendText(SharedKey + "\n"); + } + else if (SharedKeyRequired && str == "Shared Key Match") + { + StopWaitForSharedKeyTimer(); + Debug.Console(2, this, "Shared key confirmed. Ready for communication"); + OnClientReadyForcommunications(true); // Successful key exchange + } + else + { + //var bytesHandler = BytesReceived; + //if (bytesHandler != null) + // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var textHandler = TextReceived; + if (textHandler != null) + textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str)); + } } } - if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - client.ReceiveDataAsync(Receive); + catch (Exception ex) + { + Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); + } + } + if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + client.ReceiveDataAsync(Receive); + } + + void HeartbeatStart() + { + if (HeartbeatEnabled) + { + Debug.Console(2, this, "Starting Heartbeat"); + if (HeartbeatSendTimer == null) + { + + HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + } + if (HeartbeatAckTimer == null) + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } } - void HeartbeatStart() + } + void HeartbeatStop() + { + + if (HeartbeatSendTimer != null) + { + Debug.Console(2, this, "Stoping Heartbeat Send"); + HeartbeatSendTimer.Stop(); + HeartbeatSendTimer = null; + } + if (HeartbeatAckTimer != null) + { + Debug.Console(2, this, "Stoping Heartbeat Ack"); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer = null; + } + + } + void SendHeartbeat(object notused) + { + this.SendText(HeartbeatString); + Debug.Console(2, this, "Sending Heartbeat"); + + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(string received) + { + try { if (HeartbeatEnabled) { - Debug.Console(2, this, "Starting Heartbeat"); - if (HeartbeatSendTimer == null) + if (!string.IsNullOrEmpty(HeartbeatString)) { - - HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); - } - if (HeartbeatAckTimer == null) - { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); - } - } - - } - void HeartbeatStop() - { - - if (HeartbeatSendTimer != null) - { - Debug.Console(2, this, "Stoping Heartbeat Send"); - HeartbeatSendTimer.Stop(); - HeartbeatSendTimer = null; - } - if (HeartbeatAckTimer != null) - { - Debug.Console(2, this, "Stoping Heartbeat Ack"); - HeartbeatAckTimer.Stop(); - HeartbeatAckTimer = null; - } - - } - void SendHeartbeat(object notused) - { - this.SendText(HeartbeatString); - Debug.Console(2, this, "Sending Heartbeat"); - - } - - //private method to check heartbeat requirements and start or reset timer - string checkHeartbeat(string received) - { - try - { - if (HeartbeatEnabled) - { - if (!string.IsNullOrEmpty(HeartbeatString)) + var remainingText = received.Replace(HeartbeatString, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatString)) { - var remainingText = received.Replace(HeartbeatString, ""); - var noDelimiter = received.Trim(new char[] { '\r', '\n' }); - if (noDelimiter.Contains(HeartbeatString)) + if (HeartbeatAckTimer != null) { - if (HeartbeatAckTimer != null) - { - HeartbeatAckTimer.Reset(HeartbeatInterval * 2); - } - else - { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); - } - Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); - return remainingText; + HeartbeatAckTimer.Reset(HeartbeatInterval * 2); } - } - } - } - catch (Exception ex) - { - Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); - } - return received; - } - - - - void HeartbeatAckTimerFail(object o) - { - try - { - - if (IsConnected) - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); - SendText("Heartbeat not received by server, closing connection"); - CheckClosedAndTryReconnect(); - } - - } - catch (Exception ex) - { - ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); - } - } - - /// - /// - /// - void StopWaitForSharedKeyTimer() - { - if (WaitForSharedKey != null) - { - WaitForSharedKey.Stop(); - WaitForSharedKey = null; - } - } - - /// - /// SendText method - /// - public void SendText(string text) - { - if (!string.IsNullOrEmpty(text)) - { - try - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - { - Client.SendDataAsync(bytes, bytes.Length, (c, n) => + else { - // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? - if (n <= 0) - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); - } - }); + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); + return remainingText; } - } - catch (Exception ex) - { - Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); - } + } } } - - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) + catch (Exception ex) { - if (bytes.Length > 0) - { - try - { - if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - Client.SendData(bytes, bytes.Length); - } - catch (Exception ex) - { - Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); - } - } + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); } + return received; + } - /// - /// SocketStatusChange Callback - /// - /// - /// - void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) + + + void HeartbeatAckTimerFail(object o) + { + try { - if (ProgramIsStopping) + + if (IsConnected) { - ProgramIsStopping = false; - return; + Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); + SendText("Heartbeat not received by server, closing connection"); + CheckClosedAndTryReconnect(); } + + } + catch (Exception ex) + { + ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); + } + } + + /// + /// + /// + void StopWaitForSharedKeyTimer() + { + if (WaitForSharedKey != null) + { + WaitForSharedKey.Stop(); + WaitForSharedKey = null; + } + } + + /// + /// General send method + /// + public void SendText(string text) + { + if (!string.IsNullOrEmpty(text)) + { try { - Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); - - OnConnectionChange(); - - // The client could be null or disposed by this time... - if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) { - HeartbeatStop(); - OnClientReadyForcommunications(false); // socket has gone low - CheckClosedAndTryReconnect(); + Client.SendDataAsync(bytes, bytes.Length, (c, n) => + { + // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? + if (n <= 0) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); + } + }); } } catch (Exception ex) { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); + Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); } } - - /// - /// Helper for ConnectionChange event - /// - void OnConnectionChange() - { - var handler = ConnectionChange; - if (handler != null) - ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus)); - } - - /// - /// Helper to fire ClientReadyForCommunications event - /// - void OnClientReadyForcommunications(bool isReady) - { - IsReadyForCommunication = isReady; - if (this.IsReadyForCommunication) { HeartbeatStart(); } - var handler = ClientReadyForCommunications; - if (handler != null) - handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); - } - #endregion } - -} \ No newline at end of file + + /// + /// + /// + public void SendBytes(byte[] bytes) + { + if (bytes.Length > 0) + { + try + { + if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + Client.SendData(bytes, bytes.Length); + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); + } + } + } + + /// + /// SocketStatusChange Callback + /// + /// + /// + void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) + { + if (ProgramIsStopping) + { + ProgramIsStopping = false; + return; + } + try + { + Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); + + OnConnectionChange(); + + // The client could be null or disposed by this time... + if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + HeartbeatStop(); + OnClientReadyForcommunications(false); // socket has gone low + CheckClosedAndTryReconnect(); + } + } + catch (Exception ex) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); + } + } + + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + var handler = ConnectionChange; + if (handler != null) + ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus)); + } + + /// + /// Helper to fire ClientReadyForCommunications event + /// + void OnClientReadyForcommunications(bool isReady) + { + IsReadyForCommunication = isReady; + if (this.IsReadyForCommunication) { HeartbeatStart(); } + var handler = ClientReadyForCommunications; + if (handler != null) + handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); + } + #endregion +} diff --git a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs index 5cec96df..3dce8895 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs @@ -17,589 +17,563 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Generic TCP/IP server device +/// +public class GenericTcpIpServer : Device { + #region Events /// - /// Generic TCP/IP server device + /// Event for Receiving text /// - public class GenericTcpIpServer : Device + public event EventHandler TextReceived; + + /// + /// Event for client connection socket status change + /// + public event EventHandler ClientConnectionChange; + + /// + /// Event for Server State Change + /// + public event EventHandler ServerStateChange; + + /// + /// For a server with a pre shared key, this will fire after the communication is established and the key exchange is complete. If no shared key, this will fire + /// after connection is successful. Use this event to know when the client is ready for communication to avoid stepping on shared key. + /// + public event EventHandler ServerClientReadyForCommunications; + + /// + /// A band aid event to notify user that the server has choked. + /// + public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; } + + /// + /// + /// + public delegate void ServerHasChokedCallbackDelegate(); + + #endregion + + #region Properties/Variables + + /// + /// + /// + CCriticalSection ServerCCSection = new CCriticalSection(); + + + /// + /// A bandaid client that monitors whether the server is reachable + /// + GenericTcpIpClient_ForServer MonitorClient; + + /// + /// Timer to operate the bandaid monitor client in a loop. + /// + CTimer MonitorClientTimer; + + /// + /// + /// + int MonitorClientFailureCount; + + /// + /// 3 by default + /// + public int MonitorClientMaxFailureCount { get; set; } + + /// + /// Text representation of the Socket Status enum values for the server + /// + public string Status { - #region Events - /// - /// Event for Receiving text - /// - public event EventHandler TextReceived; - - /// - /// Event for client connection socket status change - /// - public event EventHandler ClientConnectionChange; - - /// - /// Event for Server State Change - /// - public event EventHandler ServerStateChange; - - /// - /// For a server with a pre shared key, this will fire after the communication is established and the key exchange is complete. If no shared key, this will fire - /// after connection is successful. Use this event to know when the client is ready for communication to avoid stepping on shared key. - /// - public event EventHandler ServerClientReadyForCommunications; - - /// - /// A band aid event to notify user that the server has choked. - /// - public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; } - - /// - /// Delegate for ServerHasChokedCallbackDelegate - /// - public delegate void ServerHasChokedCallbackDelegate(); - - #endregion - - #region Properties/Variables - - /// - /// - /// - CCriticalSection ServerCCSection = new CCriticalSection(); - - - /// - /// A bandaid client that monitors whether the server is reachable - /// - GenericTcpIpClient_ForServer MonitorClient; - - /// - /// Timer to operate the bandaid monitor client in a loop. - /// - CTimer MonitorClientTimer; - - /// - /// - /// - int MonitorClientFailureCount; - - /// - /// Gets or sets the MonitorClientMaxFailureCount - /// - public int MonitorClientMaxFailureCount { get; set; } - - /// - /// Text representation of the Socket Status enum values for the server - /// - public string Status + get { - get - { - if (myTcpServer != null) - return myTcpServer.State.ToString(); - return ServerState.SERVER_NOT_LISTENING.ToString(); - - } + if (myTcpServer != null) + return myTcpServer.State.ToString(); + return ServerState.SERVER_NOT_LISTENING.ToString(); } - /// - /// Bool showing if socket is connected - /// - public bool IsConnected + } + + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { - get - { - if (myTcpServer != null) - return (myTcpServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED; + if (myTcpServer != null) + return (myTcpServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED; + return false; + + //return (Secure ? SecureServer != null : UnsecureServer != null) && + //(Secure ? (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED : + // (UnsecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED); + } + } + + /// + /// S+ helper for IsConnected + /// + public ushort UIsConnected + { + get { return (ushort)(IsConnected ? 1 : 0); } + } + + /// + /// Bool showing if socket is connected + /// + public bool IsListening + { + get + { + if (myTcpServer != null) + return (myTcpServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING; + else return false; - - //return (Secure ? SecureServer != null : UnsecureServer != null) && - //(Secure ? (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED : - // (UnsecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED); - } + //return (Secure ? SecureServer != null : UnsecureServer != null) && + //(Secure ? (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING : + // (UnsecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING); } + } - /// - /// S+ helper for IsConnected - /// - public ushort UIsConnected + /// + /// S+ helper for IsConnected + /// + public ushort UIsListening + { + get { return (ushort)(IsListening ? 1 : 0); } + } + + /// + /// The maximum number of clients. + /// Should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable + /// + public ushort MaxClients { get; set; } + + /// + /// Number of clients currently connected. + /// + public ushort NumberOfClientsConnected + { + get { - get { return (ushort)(IsConnected ? 1 : 0); } + if (myTcpServer != null) + return (ushort)myTcpServer.NumberOfClientsConnected; + return 0; } + } - /// - /// Bool showing if socket is connected - /// - public bool IsListening + /// + /// Port Server should listen on + /// + public int Port { get; set; } + + /// + /// S+ helper for Port + /// + public ushort UPort + { + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } + + /// + /// Bool to show whether the server requires a preshared key. Must be set the same in the client, and if true shared keys must be identical on server/client + /// + public bool SharedKeyRequired { get; set; } + + /// + /// S+ helper for requires shared key bool + /// + public ushort USharedKeyRequired + { + set { - get + if (value == 1) + SharedKeyRequired = true; + else + SharedKeyRequired = false; + } + } + + /// + /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module. + /// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called + /// + public string SharedKey { get; set; } + + /// + /// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received + /// + public bool HeartbeatRequired { get; set; } + + /// + /// S+ Helper for Heartbeat Required + /// + public ushort UHeartbeatRequired + { + set + { + if (value == 1) + HeartbeatRequired = true; + else + HeartbeatRequired = false; + } + } + + /// + /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ + /// + public int HeartbeatRequiredIntervalMs { get; set; } + + /// + /// Simpl+ Heartbeat Analog value in seconds + /// + public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } } + + /// + /// String to Match for heartbeat. If null or empty any string will reset heartbeat timer + /// + public string HeartbeatStringToMatch { get; set; } + + //private timers for Heartbeats per client + Dictionary HeartbeatTimerDictionary = new Dictionary(); + + //flags to show the secure server is waiting for client at index to send the shared key + List WaitingForSharedKey = new List(); + + List ClientReadyAfterKeyExchange = new List(); + + /// + /// The connected client indexes + /// + public List ConnectedClientsIndexes = new List(); + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + /// + /// Private flag to note that the server has stopped intentionally + /// + private bool ServerStopped { get; set; } + + //Servers + TCPServer myTcpServer; + + /// + /// + /// + bool ProgramIsStopping; + + #endregion + + #region Constructors + /// + /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually. + /// + public GenericTcpIpServer() + : base("Uninitialized Dynamic TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + } + + /// + /// constructor with debug key set at instantiation. Make sure to set all properties before listening. + /// + /// + public GenericTcpIpServer(string key) + : base("Uninitialized Dynamic TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + Key = key; + } + + /// + /// Contstructor that sets all properties by calling the initialize method with a config object. + /// + /// + public GenericTcpIpServer(TcpServerConfigObject serverConfigObject) + : base("Uninitialized Dynamic TCP Server") + { + HeartbeatRequiredIntervalInSeconds = 15; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + BufferSize = 2000; + MonitorClientMaxFailureCount = 3; + Initialize(serverConfigObject); + } + #endregion + + #region Methods - Server Actions + /// + /// Disconnects all clients and stops the server + /// + public void KillServer() + { + ServerStopped = true; + if (MonitorClient != null) + { + MonitorClient.Disconnect(); + } + DisconnectAllClientsForShutdown(); + StopListening(); + } + + /// + /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ + /// + /// + public void Initialize(string key) + { + Key = key; + } + + /// + /// Initialze with server configuration object + /// + /// + public void Initialize(TcpServerConfigObject serverConfigObject) + { + try + { + if (serverConfigObject != null || string.IsNullOrEmpty(serverConfigObject.Key)) { - if (myTcpServer != null) - return (myTcpServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING; - else - return false; - //return (Secure ? SecureServer != null : UnsecureServer != null) && - //(Secure ? (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING : - // (UnsecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING); + Key = serverConfigObject.Key; + MaxClients = serverConfigObject.MaxClients; + Port = serverConfigObject.Port; + SharedKeyRequired = serverConfigObject.SharedKeyRequired; + SharedKey = serverConfigObject.SharedKey; + HeartbeatRequired = serverConfigObject.HeartbeatRequired; + HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds; + HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch; + BufferSize = serverConfigObject.BufferSize; + } - } - - /// - /// S+ helper for IsConnected - /// - public ushort UIsListening - { - get { return (ushort)(IsListening ? 1 : 0); } - } - - /// - /// The maximum number of clients. - /// Should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable - /// - public ushort MaxClients { get; set; } - - /// - /// Number of clients currently connected. - /// - public ushort NumberOfClientsConnected - { - get - { - if (myTcpServer != null) - return (ushort)myTcpServer.NumberOfClientsConnected; - return 0; - } - } - - /// - /// Gets or sets the Port - /// - public int Port { get; set; } - - /// - /// S+ helper for Port - /// - public ushort UPort - { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } - } - - /// - /// Bool to show whether the server requires a preshared key. Must be set the same in the client, and if true shared keys must be identical on server/client - /// - public bool SharedKeyRequired { get; set; } - - /// - /// S+ helper for requires shared key bool - /// - public ushort USharedKeyRequired - { - set - { - if (value == 1) - SharedKeyRequired = true; - else - SharedKeyRequired = false; - } - } - - /// - /// Gets or sets the SharedKey - /// - public string SharedKey { get; set; } - - /// - /// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received - /// - public bool HeartbeatRequired { get; set; } - - /// - /// S+ Helper for Heartbeat Required - /// - public ushort UHeartbeatRequired - { - set - { - if (value == 1) - HeartbeatRequired = true; - else - HeartbeatRequired = false; - } - } - - /// - /// Gets or sets the HeartbeatRequiredIntervalMs - /// - public int HeartbeatRequiredIntervalMs { get; set; } - - /// - /// Simpl+ Heartbeat Analog value in seconds - /// - public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } } - - /// - /// Gets or sets the HeartbeatStringToMatch - /// - public string HeartbeatStringToMatch { get; set; } - - //private timers for Heartbeats per client - Dictionary HeartbeatTimerDictionary = new Dictionary(); - - //flags to show the secure server is waiting for client at index to send the shared key - List WaitingForSharedKey = new List(); - - List ClientReadyAfterKeyExchange = new List(); - - /// - /// The connected client indexes - /// - public List ConnectedClientsIndexes = new List(); - - /// - /// Gets or sets the BufferSize - /// - public int BufferSize { get; set; } - - /// - /// Private flag to note that the server has stopped intentionally - /// - private bool ServerStopped { get; set; } - - //Servers - TCPServer myTcpServer; - - /// - /// - /// - bool ProgramIsStopping; - - #endregion - - #region Constructors - /// - /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually. - /// - public GenericTcpIpServer() - : base("Uninitialized Dynamic TCP Server") - { - HeartbeatRequiredIntervalInSeconds = 15; - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - BufferSize = 2000; - MonitorClientMaxFailureCount = 3; - } - - /// - /// constructor with debug key set at instantiation. Make sure to set all properties before listening. - /// - /// - public GenericTcpIpServer(string key) - : base("Uninitialized Dynamic TCP Server") - { - HeartbeatRequiredIntervalInSeconds = 15; - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - BufferSize = 2000; - MonitorClientMaxFailureCount = 3; - Key = key; - } - - /// - /// Contstructor that sets all properties by calling the initialize method with a config object. - /// - /// - public GenericTcpIpServer(TcpServerConfigObject serverConfigObject) - : base("Uninitialized Dynamic TCP Server") - { - HeartbeatRequiredIntervalInSeconds = 15; - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - BufferSize = 2000; - MonitorClientMaxFailureCount = 3; - Initialize(serverConfigObject); - } - #endregion - - #region Methods - Server Actions - /// - /// KillServer method - /// - public void KillServer() - { - ServerStopped = true; - if (MonitorClient != null) - { - MonitorClient.Disconnect(); - } - DisconnectAllClientsForShutdown(); - StopListening(); - } - - /// - /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ - /// - /// - /// - /// Initialize method - /// - public void Initialize(string key) - { - Key = key; - } - - /// - /// Initialze with server configuration object - /// - /// - public void Initialize(TcpServerConfigObject serverConfigObject) - { - try - { - if (serverConfigObject != null || string.IsNullOrEmpty(serverConfigObject.Key)) - { - Key = serverConfigObject.Key; - MaxClients = serverConfigObject.MaxClients; - Port = serverConfigObject.Port; - SharedKeyRequired = serverConfigObject.SharedKeyRequired; - SharedKey = serverConfigObject.SharedKey; - HeartbeatRequired = serverConfigObject.HeartbeatRequired; - HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds; - HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch; - BufferSize = serverConfigObject.BufferSize; - - } - else - { - ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); - } - } - catch + else { ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); } } - - /// - /// Listen method - /// - public void Listen() + catch { - ServerCCSection.Enter(); - try - { - if (Port < 1 || Port > 65535) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); - ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); - return; - } - if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); - ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); - return; - } - if (IsListening) - return; + ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); + } + } - if (myTcpServer == null) - { - myTcpServer = new TCPServer(Port, MaxClients); - if(HeartbeatRequired) - myTcpServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5); - + /// + /// Start listening on the specified port + /// + public void Listen() + { + ServerCCSection.Enter(); + try + { + if (Port < 1 || Port > 65535) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); + ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); + ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); + return; + } + if (IsListening) + return; + + if (myTcpServer == null) + { + myTcpServer = new TCPServer(Port, MaxClients); + if(HeartbeatRequired) + myTcpServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5); + // myTcpServer.HandshakeTimeout = 30; - } - else - { - KillServer(); - myTcpServer.PortNumber = Port; - } - - myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange; - myTcpServer.SocketStatusChange += TcpServer_SocketStatusChange; - - ServerStopped = false; - myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); - OnServerStateChange(myTcpServer.State); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus); - - // StartMonitorClient(); - - - ServerCCSection.Leave(); } - catch (Exception ex) + else { - ServerCCSection.Leave(); - ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key); + KillServer(); + myTcpServer.PortNumber = Port; } + + myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange; + myTcpServer.SocketStatusChange += TcpServer_SocketStatusChange; + + ServerStopped = false; + myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); + OnServerStateChange(myTcpServer.State); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus); + + // StartMonitorClient(); + + + ServerCCSection.Leave(); } - - /// - /// StopListening method - /// - public void StopListening() + catch (Exception ex) { - try - { - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); - if (myTcpServer != null) - { - myTcpServer.Stop(); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State); - OnServerStateChange(myTcpServer.State); - } - ServerStopped = true; - } - catch (Exception ex) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); - } + ServerCCSection.Leave(); + ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key); } + } - /// - /// Disconnects Client - /// - /// - /// - /// DisconnectClient method - /// - public void DisconnectClient(uint client) + /// + /// Stop Listening + /// + public void StopListening() + { + try { - try - { - myTcpServer.Disconnect(client); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); - } - catch (Exception ex) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); - } - } - /// - /// DisconnectAllClientsForShutdown method - /// - public void DisconnectAllClientsForShutdown() - { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); if (myTcpServer != null) { - myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange; - foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly - { - var i = index; - if (!myTcpServer.ClientConnected(index)) - continue; - try - { - myTcpServer.Disconnect(i); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); - } - catch (Exception ex) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); - } - } - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus); + myTcpServer.Stop(); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State); + OnServerStateChange(myTcpServer.State); } + ServerStopped = true; + } + catch (Exception ex) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); + } + } - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); - ConnectedClientsIndexes.Clear(); - - if (!ProgramIsStopping) + /// + /// Disconnects Client + /// + /// + public void DisconnectClient(uint client) + { + try + { + myTcpServer.Disconnect(client); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); + } + catch (Exception ex) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); + } + } + /// + /// Disconnect All Clients + /// + public void DisconnectAllClientsForShutdown() + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); + if (myTcpServer != null) + { + myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange; + foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly { - OnConnectionChange(); - OnServerStateChange(myTcpServer.State); //State shows both listening and connected + var i = index; + if (!myTcpServer.ClientConnected(index)) + continue; + try + { + myTcpServer.Disconnect(i); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); + } + catch (Exception ex) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); + } } - - // var o = new { }; + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus); } - /// - /// Broadcast text from server to all connected clients - /// - /// - /// - /// BroadcastText method - /// - public void BroadcastText(string text) + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); + ConnectedClientsIndexes.Clear(); + + if (!ProgramIsStopping) { - CCriticalSection CCBroadcast = new CCriticalSection(); - CCBroadcast.Enter(); - try - { - if (ConnectedClientsIndexes.Count > 0) - { - byte[] b = Encoding.GetEncoding(28591).GetBytes(text); - foreach (uint i in ConnectedClientsIndexes) - { - if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i))) - { - SocketErrorCodes error = myTcpServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); - if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) - this.LogError("{error}",error.ToString()); - } - } - } - CCBroadcast.Leave(); - } - catch (Exception ex) - { - CCBroadcast.Leave(); - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message); - } + OnConnectionChange(); + OnServerStateChange(myTcpServer.State); //State shows both listening and connected } - /// - /// Not sure this is useful in library, maybe Pro?? - /// - /// - /// - /// - /// SendTextToClient method - /// - public void SendTextToClient(string text, uint clientIndex) + // var o = new { }; + } + + /// + /// Broadcast text from server to all connected clients + /// + /// + public void BroadcastText(string text) + { + CCriticalSection CCBroadcast = new CCriticalSection(); + CCBroadcast.Enter(); + try { - try + if (ConnectedClientsIndexes.Count > 0) { byte[] b = Encoding.GetEncoding(28591).GetBytes(text); - if (myTcpServer != null && myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + foreach (uint i in ConnectedClientsIndexes) { - if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) - myTcpServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i))) + { + SocketErrorCodes error = myTcpServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); + if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) + this.LogError("{error}",error.ToString()); + } } } - catch (Exception ex) + CCBroadcast.Leave(); + } + catch (Exception ex) + { + CCBroadcast.Leave(); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message); + } + } + + /// + /// Not sure this is useful in library, maybe Pro?? + /// + /// + /// + public void SendTextToClient(string text, uint clientIndex) + { + try + { + byte[] b = Encoding.GetEncoding(28591).GetBytes(text); + if (myTcpServer != null && myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) { - Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) + myTcpServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); } } - - //private method to check heartbeat requirements and start or reset timer - string checkHeartbeat(uint clientIndex, string received) + catch (Exception ex) { - try + Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + } + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(uint clientIndex, string received) + { + try + { + if (HeartbeatRequired) { - if (HeartbeatRequired) + if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) { - if (!string.IsNullOrEmpty(HeartbeatStringToMatch)) - { - var remainingText = received.Replace(HeartbeatStringToMatch, ""); - var noDelimiter = received.Trim(new char[] { '\r', '\n' }); - if (noDelimiter.Contains(HeartbeatStringToMatch)) - { - if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) - HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); - else - { - CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); - HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); - } - Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); - // Return Heartbeat - SendTextToClient(HeartbeatStringToMatch, clientIndex); - return remainingText; - } - } - else + var remainingText = received.Replace(HeartbeatStringToMatch, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatStringToMatch)) { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); @@ -608,200 +582,212 @@ namespace PepperDash.Core CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } - Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); + Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); + // Return Heartbeat + SendTextToClient(HeartbeatStringToMatch, clientIndex); + return remainingText; } } - } - catch (Exception ex) - { - Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); - } - return received; - } - - /// - /// Gets the IP address based on the client index - /// - /// - /// IP address of the client - /// - /// GetClientIPAddress method - /// - public string GetClientIPAddress(uint clientIndex) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); - if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) - { - var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); - return ipa; - - } - else - { - return ""; + else + { + if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) + HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); + else + { + CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); + HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + } + Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); + } } } - - #endregion - - #region Methods - HeartbeatTimer Callback - - void HeartbeatTimer_CallbackFunction(object o) + catch (Exception ex) { - uint clientIndex = 99999; - string address = string.Empty; - try + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + } + return received; + } + + /// + /// Gets the IP address based on the client index + /// + /// + /// IP address of the client + public string GetClientIPAddress(uint clientIndex) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); + if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) + { + var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); + return ipa; + + } + else + { + return ""; + } + } + + #endregion + + #region Methods - HeartbeatTimer Callback + + void HeartbeatTimer_CallbackFunction(object o) + { + uint clientIndex = 99999; + string address = string.Empty; + try + { + clientIndex = (uint)o; + address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", + address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); + + if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); + + var discoResult = myTcpServer.Disconnect(clientIndex); + //Debug.Console(1, this, "{0}", discoResult); + + if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) { - clientIndex = (uint)o; - address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Dispose(); + HeartbeatTimerDictionary.Remove(clientIndex); + } + } + catch (Exception ex) + { + ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key); + } + } - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", - address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); + #endregion - if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) - SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); + #region Methods - Socket Status Changed Callbacks + /// + /// Secure Server Socket Status Changed Callback + /// + /// + /// + /// + void TcpServer_SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus serverSocketStatus) + { + try + { - var discoResult = myTcpServer.Disconnect(clientIndex); - //Debug.Console(1, this, "{0}", discoResult); - - if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + if (ConnectedClientsIndexes.Contains(clientIndex)) + ConnectedClientsIndexes.Remove(clientIndex); + if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) { HeartbeatTimerDictionary[clientIndex].Stop(); HeartbeatTimerDictionary[clientIndex].Dispose(); HeartbeatTimerDictionary.Remove(clientIndex); } - } - catch (Exception ex) - { - ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key); - } - } - - #endregion - - #region Methods - Socket Status Changed Callbacks - /// - /// Secure Server Socket Status Changed Callback - /// - /// - /// - /// - void TcpServer_SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus serverSocketStatus) - { - try - { - - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); - if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) - { - if (ConnectedClientsIndexes.Contains(clientIndex)) - ConnectedClientsIndexes.Remove(clientIndex); - if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex)) - { - HeartbeatTimerDictionary[clientIndex].Stop(); - HeartbeatTimerDictionary[clientIndex].Dispose(); - HeartbeatTimerDictionary.Remove(clientIndex); - } - if (ClientReadyAfterKeyExchange.Contains(clientIndex)) - ClientReadyAfterKeyExchange.Remove(clientIndex); + if (ClientReadyAfterKeyExchange.Contains(clientIndex)) + ClientReadyAfterKeyExchange.Remove(clientIndex); if (WaitingForSharedKey.Contains(clientIndex)) WaitingForSharedKey.Remove(clientIndex); - } - } - catch (Exception ex) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); - } - onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); - } - - #endregion - - #region Methods Connected Callbacks - /// - /// Secure TCP Client Connected to Secure Server Callback - /// - /// - /// - void TcpConnectCallback(TCPServer server, uint clientIndex) - { - try - { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", - server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), - clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); - if (clientIndex != 0) - { - if (server.ClientConnected(clientIndex)) - { - - if (!ConnectedClientsIndexes.Contains(clientIndex)) - { - ConnectedClientsIndexes.Add(clientIndex); - } - if (SharedKeyRequired) - { - if (!WaitingForSharedKey.Contains(clientIndex)) - { - WaitingForSharedKey.Add(clientIndex); - } - byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); - server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); - } - else - { - OnServerClientReadyForCommunications(clientIndex); - } - if (HeartbeatRequired) - { - if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) - { - HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); - } - } - - server.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback); - } - } - else - { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); - if (!ServerStopped) - { - server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); - return; - } - } - } - catch (Exception ex) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); - } - //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", - // server.State, - // MaxClients, - // ServerStopped); - if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection"); - server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); - } } - - #endregion - - #region Methods - Send/Receive Callbacks - /// - /// Secure Received Data Async Callback - /// - /// - /// - /// - void TcpServerReceivedDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesReceived) + catch (Exception ex) { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); + } + onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); + } + + #endregion + + #region Methods Connected Callbacks + /// + /// Secure TCP Client Connected to Secure Server Callback + /// + /// + /// + void TcpConnectCallback(TCPServer server, uint clientIndex) + { + try + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", + server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), + clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); + if (clientIndex != 0) + { + if (server.ClientConnected(clientIndex)) + { + + if (!ConnectedClientsIndexes.Contains(clientIndex)) + { + ConnectedClientsIndexes.Add(clientIndex); + } + if (SharedKeyRequired) + { + if (!WaitingForSharedKey.Contains(clientIndex)) + { + WaitingForSharedKey.Add(clientIndex); + } + byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); + server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + } + else + { + OnServerClientReadyForCommunications(clientIndex); + } + if (HeartbeatRequired) + { + if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) + { + HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); + } + } + + server.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback); + } + } + else + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); + if (!ServerStopped) + { + server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); + return; + } + } + } + catch (Exception ex) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + } + //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", + // server.State, + // MaxClients, + // ServerStopped); + if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection"); + server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); + + } + } + + #endregion + + #region Methods - Send/Receive Callbacks + /// + /// Secure Received Data Async Callback + /// + /// + /// + /// + void TcpServerReceivedDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesReceived) + { if (numberOfBytesReceived > 0) { string received = "Nothing"; @@ -845,181 +831,180 @@ namespace PepperDash.Core myTCPServer.Disconnect(); } - } + } - #endregion + #endregion - #region Methods - EventHelpers/Callbacks + #region Methods - EventHelpers/Callbacks - //Private Helper method to call the Connection Change Event - void onConnectionChange(uint clientIndex, SocketStatus clientStatus) + //Private Helper method to call the Connection Change Event + void onConnectionChange(uint clientIndex, SocketStatus clientStatus) + { + if (clientIndex != 0) //0 is error not valid client change { - if (clientIndex != 0) //0 is error not valid client change - { - var handler = ClientConnectionChange; - if (handler != null) - { - handler(this, new GenericTcpServerSocketStatusChangeEventArgs(myTcpServer, clientIndex, clientStatus)); - } - } - } - - //Private Helper method to call the Connection Change Event - void OnConnectionChange() - { - if (ProgramIsStopping) - { - return; - } var handler = ClientConnectionChange; if (handler != null) { - handler(this, new GenericTcpServerSocketStatusChangeEventArgs()); + handler(this, new GenericTcpServerSocketStatusChangeEventArgs(myTcpServer, clientIndex, clientStatus)); } } + } - //Private Helper Method to call the Text Received Event - void onTextReceived(string text, uint clientIndex) + //Private Helper method to call the Connection Change Event + void OnConnectionChange() + { + if (ProgramIsStopping) { - var handler = TextReceived; - if (handler != null) - handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex)); + return; } - - //Private Helper Method to call the Server State Change Event - void OnServerStateChange(ServerState state) + var handler = ClientConnectionChange; + if (handler != null) { - if (ProgramIsStopping) - { - return; - } - var handler = ServerStateChange; - if (handler != null) - { - handler(this, new GenericTcpServerStateChangedEventArgs(state)); - } + handler(this, new GenericTcpServerSocketStatusChangeEventArgs()); } + } - /// - /// Private Event Handler method to handle the closing of connections when the program stops - /// - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + //Private Helper Method to call the Text Received Event + void onTextReceived(string text, uint clientIndex) + { + var handler = TextReceived; + if (handler != null) + handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex)); + } + + //Private Helper Method to call the Server State Change Event + void OnServerStateChange(ServerState state) + { + if (ProgramIsStopping) { - if (programEventType == eProgramStatusEventType.Stopping) - { - ProgramIsStopping = true; - // kill bandaid things - if (MonitorClientTimer != null) - MonitorClientTimer.Stop(); - if (MonitorClient != null) - MonitorClient.Disconnect(); - - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); - KillServer(); - } + return; } - - //Private event handler method to raise the event that the server is ready to send data after a successful client shared key negotiation - void OnServerClientReadyForCommunications(uint clientIndex) + var handler = ServerStateChange; + if (handler != null) { - ClientReadyAfterKeyExchange.Add(clientIndex); - var handler = ServerClientReadyForCommunications; - if (handler != null) - handler(this, new GenericTcpServerSocketStatusChangeEventArgs( - this, clientIndex, myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex))); + handler(this, new GenericTcpServerStateChangedEventArgs(state)); } - #endregion + } - #region Monitor Client - /// - /// Starts the monitor client cycle. Timed wait, then call RunMonitorClient - /// - void StartMonitorClient() + /// + /// Private Event Handler method to handle the closing of connections when the program stops + /// + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) { + ProgramIsStopping = true; + // kill bandaid things if (MonitorClientTimer != null) - { - return; - } - MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); - } - - /// - /// - /// - void RunMonitorClient() - { - MonitorClient = new GenericTcpIpClient_ForServer(Key + "-MONITOR", "127.0.0.1", Port, 2000); - MonitorClient.SharedKeyRequired = this.SharedKeyRequired; - MonitorClient.SharedKey = this.SharedKey; - MonitorClient.ConnectionHasHungCallback = MonitorClientHasHungCallback; - //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; - MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; - - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); - - MonitorClient.Connect(); - // From here MonitorCLient either connects or hangs, MonitorClient will call back - - } - - /// - /// - /// - void StopMonitorClient() - { - if (MonitorClient == null) - return; - - MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm; - MonitorClient.Disconnect(); - MonitorClient = null; - } - - /// - /// On monitor connect, restart the operation - /// - void MonitorClient_IsReadyForComm(object sender, GenericTcpServerClientReadyForcommunicationsEventArgs args) - { - if (args.IsReady) - { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); MonitorClientTimer.Stop(); - MonitorClientTimer = null; - MonitorClientFailureCount = 0; - CrestronEnvironment.Sleep(2000); - StopMonitorClient(); - StartMonitorClient(); - } - } + if (MonitorClient != null) + MonitorClient.Disconnect(); - /// - /// If the client hangs, add to counter and maybe fire the choke event - /// - void MonitorClientHasHungCallback() + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); + KillServer(); + } + } + + //Private event handler method to raise the event that the server is ready to send data after a successful client shared key negotiation + void OnServerClientReadyForCommunications(uint clientIndex) + { + ClientReadyAfterKeyExchange.Add(clientIndex); + var handler = ServerClientReadyForCommunications; + if (handler != null) + handler(this, new GenericTcpServerSocketStatusChangeEventArgs( + this, clientIndex, myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex))); + } + #endregion + + #region Monitor Client + /// + /// Starts the monitor client cycle. Timed wait, then call RunMonitorClient + /// + void StartMonitorClient() + { + if (MonitorClientTimer != null) { - MonitorClientFailureCount++; + return; + } + MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); + } + + /// + /// + /// + void RunMonitorClient() + { + MonitorClient = new GenericTcpIpClient_ForServer(Key + "-MONITOR", "127.0.0.1", Port, 2000); + MonitorClient.SharedKeyRequired = this.SharedKeyRequired; + MonitorClient.SharedKey = this.SharedKey; + MonitorClient.ConnectionHasHungCallback = MonitorClientHasHungCallback; + //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; + MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; + + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); + + MonitorClient.Connect(); + // From here MonitorCLient either connects or hangs, MonitorClient will call back + + } + + /// + /// + /// + void StopMonitorClient() + { + if (MonitorClient == null) + return; + + MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm; + MonitorClient.Disconnect(); + MonitorClient = null; + } + + /// + /// On monitor connect, restart the operation + /// + void MonitorClient_IsReadyForComm(object sender, GenericTcpServerClientReadyForcommunicationsEventArgs args) + { + if (args.IsReady) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); MonitorClientTimer.Stop(); MonitorClientTimer = null; + MonitorClientFailureCount = 0; + CrestronEnvironment.Sleep(2000); StopMonitorClient(); - if (MonitorClientFailureCount < MonitorClientMaxFailureCount) - { - Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", - MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); - StartMonitorClient(); - } - else - { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, - "\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************", - MonitorClientMaxFailureCount); - - var handler = ServerHasChoked; - if (handler != null) - handler(); - // Some external thing is in charge here. Expected reset of program - } + StartMonitorClient(); } - #endregion } + + /// + /// If the client hangs, add to counter and maybe fire the choke event + /// + void MonitorClientHasHungCallback() + { + MonitorClientFailureCount++; + MonitorClientTimer.Stop(); + MonitorClientTimer = null; + StopMonitorClient(); + if (MonitorClientFailureCount < MonitorClientMaxFailureCount) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", + MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); + StartMonitorClient(); + } + else + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, + "\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************", + MonitorClientMaxFailureCount); + + var handler = ServerHasChoked; + if (handler != null) + handler(); + // Some external thing is in charge here. Expected reset of program + } + } + #endregion } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/GenericUdpServer.cs b/src/PepperDash.Core/Comm/GenericUdpServer.cs index 34df1444..6a761e6c 100644 --- a/src/PepperDash.Core/Comm/GenericUdpServer.cs +++ b/src/PepperDash.Core/Comm/GenericUdpServer.cs @@ -11,364 +11,347 @@ using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using PepperDash.Core.Logging; using Required = NewtonsoftJson::Newtonsoft.Json.Required; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Generic UDP Server device +/// +public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging { + private const string SplusKey = "Uninitialized Udp Server"; /// - /// Generic UDP Server device + /// Object to enable stream debugging /// - public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging - { - private const string SplusKey = "Uninitialized Udp Server"; - /// - /// Object to enable stream debugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } - /// - /// - /// - public event EventHandler BytesReceived; + public CommunicationStreamDebugging StreamDebugging { get; private set; } + /// + /// + /// + public event EventHandler BytesReceived; - /// - /// - /// - public event EventHandler TextReceived; + /// + /// + /// + public event EventHandler TextReceived; - /// - /// This event will fire when a message is dequeued that includes the source IP and Port info if needed to determine the source of the received data. - /// + /// + /// This event will fire when a message is dequeued that includes the source IP and Port info if needed to determine the source of the received data. + /// public event EventHandler DataRecievedExtra; - /// - /// - /// - public event EventHandler ConnectionChange; + /// + /// + /// + public event EventHandler ConnectionChange; - /// - /// - /// - public event EventHandler UpdateConnectionStatus; + /// + /// + /// + public event EventHandler UpdateConnectionStatus; - /// - /// - /// - public SocketStatus ClientStatus + /// + /// + /// + public SocketStatus ClientStatus + { + get { - get - { - return Server.ServerStatus; - } + return Server.ServerStatus; } - - /// - /// - /// - public ushort UStatus - { - get { return (ushort)Server.ServerStatus; } - } - - /// - /// Address of server - /// - public string Hostname { get; set; } - - - /// - /// Port on server - /// - public int Port { get; set; } - - /// - /// Another damn S+ helper because S+ seems to treat large port nums as signed ints - /// which screws up things - /// - public ushort UPort - { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } - } - - /// - /// Indicates that the UDP Server is enabled - /// - public bool IsConnected - { - get; - private set; - } - - /// - /// Numeric value indicating - /// - public ushort UIsConnected - { - get { return IsConnected ? (ushort)1 : (ushort)0; } - } - - /// - /// Defaults to 2000 - /// - public int BufferSize { get; set; } - - /// - /// The server - /// - public UDPServer Server { get; private set; } - - /// - /// Constructor for S+. Make sure to set key, address, port, and buffersize using init method - /// - public GenericUdpServer() - : base(SplusKey) - { - StreamDebugging = new CommunicationStreamDebugging(SplusKey); - BufferSize = 5000; - - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); - } - - /// - /// - /// - /// - /// - /// - /// - public GenericUdpServer(string key, string address, int port, int bufferSize) - : base(key) - { - StreamDebugging = new CommunicationStreamDebugging(key); - Hostname = address; - Port = port; - BufferSize = bufferSize; - - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); - } - - /// - /// Call from S+ to initialize values - /// - /// - /// - /// - /// - /// Initialize method - /// - public void Initialize(string key, string address, ushort port) - { - Key = key; - Hostname = address; - UPort = port; - } - - /// - /// - /// - /// - void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) - { - // Re-enable the server if the link comes back up and the status should be connected - if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp - && IsConnected) - { - Connect(); - } - } - - /// - /// - /// - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType != eProgramStatusEventType.Stopping) - return; - - Debug.Console(1, this, "Program stopping. Disabling Server"); - Disconnect(); - } - - /// - /// Connect method - /// - public void Connect() - { - if (Server == null) - { - try - { - var address = IPAddress.Parse(Hostname); - - Server = new UDPServer(address, Port, BufferSize); - - } - catch (Exception ex) - { - this.LogError("Error parsing IP Address '{ipAddress}': message: {message}", Hostname, ex.Message); - this.LogInformation("Creating UDPServer with default buffersize"); - - Server = new UDPServer(); - } - - } - - if (string.IsNullOrEmpty(Hostname)) - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key); - return; - } - if (Port < 1 || Port > 65535) - { - { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key); - return; - } - } - - var status = Server.EnableUDPServer(Hostname, Port); - - Debug.Console(2, this, "SocketErrorCode: {0}", status); - if (status == SocketErrorCodes.SOCKET_OK) - IsConnected = true; - - var handler = UpdateConnectionStatus; - if (handler != null) - handler(this, new GenericUdpConnectedEventArgs(UIsConnected)); - - // Start receiving data - Server.ReceiveDataAsync(Receive); - } - - /// - /// Disconnect method - /// - public void Disconnect() - { - if (Server != null) - Server.DisableUDPServer(); - - IsConnected = false; - - var handler = UpdateConnectionStatus; - if (handler != null) - handler(this, new GenericUdpConnectedEventArgs(UIsConnected)); - } - - - /// - /// Recursive method to receive data - /// - /// - /// - void Receive(UDPServer server, int numBytes) - { - Debug.Console(2, this, "Received {0} bytes", numBytes); - - try - { - if (numBytes <= 0) - return; - - var sourceIp = Server.IPAddressLastMessageReceivedFrom; - var sourcePort = Server.IPPortLastMessageReceivedFrom; - var bytes = server.IncomingDataBuffer.Take(numBytes).ToArray(); - var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - - var dataRecivedExtra = DataRecievedExtra; - if (dataRecivedExtra != null) - dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes)); - - Debug.Console(2, this, "Bytes: {0}", bytes.ToString()); - var bytesHandler = BytesReceived; - if (bytesHandler != null) - { - this.PrintReceivedBytes(bytes); - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - } - var textHandler = TextReceived; - if (textHandler != null) - { - this.PrintReceivedText(str); - textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - } - } - catch (Exception ex) - { - this.LogException(ex, "GenericUdpServer Receive error"); - } - finally - { - server.ReceiveDataAsync(Receive); - } - } - - /// - /// General send method - /// - /// - /// - /// SendText method - /// - public void SendText(string text) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - - if (IsConnected && Server != null) - { - this.PrintSentText(text); - - Server.SendData(bytes, bytes.Length); - } - } - - /// - /// - /// - /// - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) - { - this.PrintSentBytes(bytes); - - if (IsConnected && Server != null) - Server.SendData(bytes, bytes.Length); - } - } /// /// Represents a GenericUdpReceiveTextExtraArgs /// - public class GenericUdpReceiveTextExtraArgs : EventArgs + public ushort UStatus { - /// - /// - /// + get { return (ushort)Server.ServerStatus; } + } + + /// + /// Address of server + /// + public string Hostname { get; set; } + + + /// + /// Port on server + /// + public int Port { get; set; } + + /// + /// Another damn S+ helper because S+ seems to treat large port nums as signed ints + /// which screws up things + /// + public ushort UPort + { + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } + + /// + /// Indicates that the UDP Server is enabled + /// + public bool IsConnected + { + get; + private set; + } + + /// + /// Numeric value indicating + /// + public ushort UIsConnected + { + get { return IsConnected ? (ushort)1 : (ushort)0; } + } + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + /// + /// The server + /// + public UDPServer Server { get; private set; } + + /// + /// Constructor for S+. Make sure to set key, address, port, and buffersize using init method + /// + public GenericUdpServer() + : base(SplusKey) + { + StreamDebugging = new CommunicationStreamDebugging(SplusKey); + BufferSize = 5000; + + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); + } + + /// + /// + /// + /// + /// + /// + /// + public GenericUdpServer(string key, string address, int port, int buffefSize) + : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); + Hostname = address; + Port = port; + BufferSize = buffefSize; + + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); + } + + /// + /// Call from S+ to initialize values + /// + /// + /// + /// + public void Initialize(string key, string address, ushort port) + { + Key = key; + Hostname = address; + UPort = port; + } + + /// + /// + /// + /// + void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) + { + // Re-enable the server if the link comes back up and the status should be connected + if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp + && IsConnected) + { + Connect(); + } + } + + /// + /// + /// + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType != eProgramStatusEventType.Stopping) + return; + + Debug.Console(1, this, "Program stopping. Disabling Server"); + Disconnect(); + } + + /// + /// Enables the UDP Server + /// + public void Connect() + { + if (Server == null) + { + Server = new UDPServer(); + } + + if (string.IsNullOrEmpty(Hostname)) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key); + return; + } + if (Port < 1 || Port > 65535) + { + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key); + return; + } + } + + var status = Server.EnableUDPServer(Hostname, Port); + + Debug.Console(2, this, "SocketErrorCode: {0}", status); + if (status == SocketErrorCodes.SOCKET_OK) + IsConnected = true; + + var handler = UpdateConnectionStatus; + if (handler != null) + handler(this, new GenericUdpConnectedEventArgs(UIsConnected)); + + // Start receiving data + Server.ReceiveDataAsync(Receive); + } + + /// + /// Disabled the UDP Server + /// + public void Disconnect() + { + if(Server != null) + Server.DisableUDPServer(); + + IsConnected = false; + + var handler = UpdateConnectionStatus; + if (handler != null) + handler(this, new GenericUdpConnectedEventArgs(UIsConnected)); + } + + + /// + /// Recursive method to receive data + /// + /// + /// + void Receive(UDPServer server, int numBytes) + { + Debug.Console(2, this, "Received {0} bytes", numBytes); + + try + { + if (numBytes <= 0) + return; + + var sourceIp = Server.IPAddressLastMessageReceivedFrom; + var sourcePort = Server.IPPortLastMessageReceivedFrom; + var bytes = server.IncomingDataBuffer.Take(numBytes).ToArray(); + var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + + var dataRecivedExtra = DataRecievedExtra; + if (dataRecivedExtra != null) + dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes)); + + Debug.Console(2, this, "Bytes: {0}", bytes.ToString()); + var bytesHandler = BytesReceived; + if (bytesHandler != null) + { + if (StreamDebugging.RxStreamDebuggingIsEnabled) + { + Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); + } + 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); + textHandler(this, new GenericCommMethodReceiveTextArgs(str)); + } + } + catch (Exception ex) + { + this.LogException(ex, "GenericUdpServer Receive error"); + } + finally + { + server.ReceiveDataAsync(Receive); + } + } + + /// + /// General send method + /// + /// + public void SendText(string text) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + + if (IsConnected && Server != null) + { + if (StreamDebugging.TxStreamDebuggingIsEnabled) + Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); + + Server.SendData(bytes, bytes.Length); + } + } + + /// + /// + /// + /// + public void SendBytes(byte[] bytes) + { + if (StreamDebugging.TxStreamDebuggingIsEnabled) + Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + + if (IsConnected && Server != null) + Server.SendData(bytes, bytes.Length); + } + +} + +/// +/// +/// + public class GenericUdpReceiveTextExtraArgs : EventArgs + { + /// + /// + /// public string Text { get; private set; } - /// - /// - /// + /// + /// + /// public string IpAddress { get; private set; } - /// - /// - /// - public int Port { get; private set; } - /// - /// - /// + /// + /// + /// + public int Port { get; private set; } + /// + /// + /// public byte[] Bytes { get; private set; } - /// - /// - /// - /// - /// - /// - /// + /// + /// + /// + /// + /// + /// + /// public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes) { Text = text; @@ -383,34 +366,33 @@ namespace PepperDash.Core public GenericUdpReceiveTextExtraArgs() { } } +/// +/// +/// +public class UdpServerPropertiesConfig +{ /// /// /// - public class UdpServerPropertiesConfig + [JsonProperty(Required = Required.Always)] + public string Address { get; set; } + + /// + /// + /// + [JsonProperty(Required = Required.Always)] + public int Port { get; set; } + + /// + /// Defaults to 32768 + /// + public int BufferSize { get; set; } + + /// + /// + /// + public UdpServerPropertiesConfig() { - /// - /// - /// - [JsonProperty(Required = Required.Always)] - public string Address { get; set; } - - /// - /// - /// - [JsonProperty(Required = Required.Always)] - public int Port { get; set; } - - /// - /// Defaults to 32768 - /// - public int BufferSize { get; set; } - - /// - /// - /// - public UdpServerPropertiesConfig() - { - BufferSize = 32768; - } + BufferSize = 32768; } } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/TcpClientConfigObject.cs b/src/PepperDash.Core/Comm/TcpClientConfigObject.cs index e2137d4a..2aec5af8 100644 --- a/src/PepperDash.Core/Comm/TcpClientConfigObject.cs +++ b/src/PepperDash.Core/Comm/TcpClientConfigObject.cs @@ -2,60 +2,59 @@ using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Client config object for TCP client with server that inherits from TcpSshPropertiesConfig and adds properties for shared key and heartbeat +/// +public class TcpClientConfigObject { /// - /// Represents a TcpClientConfigObject + /// TcpSsh Properties /// - public class TcpClientConfigObject - { - /// - /// TcpSsh Properties - /// - [JsonProperty("control")] - public ControlPropertiesConfig Control { get; set; } + [JsonProperty("control")] + public ControlPropertiesConfig Control { get; set; } - /// - /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic - /// - [JsonProperty("secure")] - public bool Secure { get; set; } + /// + /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic + /// + [JsonProperty("secure")] + public bool Secure { get; set; } - /// - /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client - /// - [JsonProperty("sharedKeyRequired")] - public bool SharedKeyRequired { get; set; } + /// + /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client + /// + [JsonProperty("sharedKeyRequired")] + public bool SharedKeyRequired { get; set; } - /// - /// The shared key that must match on the server and client - /// - [JsonProperty("sharedKey")] - public string SharedKey { get; set; } + /// + /// The shared key that must match on the server and client + /// + [JsonProperty("sharedKey")] + public string SharedKey { get; set; } - /// - /// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received. - /// heartbeats do not raise received events. - /// - [JsonProperty("heartbeatRequired")] - public bool HeartbeatRequired { get; set; } + /// + /// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received. + /// heartbeats do not raise received events. + /// + [JsonProperty("heartbeatRequired")] + public bool HeartbeatRequired { get; set; } - /// - /// The interval in seconds for the heartbeat from the client. If not received client is disconnected - /// - [JsonProperty("heartbeatRequiredIntervalInSeconds")] - public ushort HeartbeatRequiredIntervalInSeconds { get; set; } + /// + /// The interval in seconds for the heartbeat from the client. If not received client is disconnected + /// + [JsonProperty("heartbeatRequiredIntervalInSeconds")] + public ushort HeartbeatRequiredIntervalInSeconds { get; set; } - /// - /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided. - /// - [JsonProperty("heartbeatStringToMatch")] - public string HeartbeatStringToMatch { get; set; } + /// + /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided. + /// + [JsonProperty("heartbeatStringToMatch")] + public string HeartbeatStringToMatch { get; set; } - /// - /// Receive Queue size must be greater than 20 or defaults to 20 - /// - [JsonProperty("receiveQueueSize")] - public int ReceiveQueueSize { get; set; } - } + /// + /// Receive Queue size must be greater than 20 or defaults to 20 + /// + [JsonProperty("receiveQueueSize")] + public int ReceiveQueueSize { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/TcpServerConfigObject.cs b/src/PepperDash.Core/Comm/TcpServerConfigObject.cs index 043cf58d..b60f486f 100644 --- a/src/PepperDash.Core/Comm/TcpServerConfigObject.cs +++ b/src/PepperDash.Core/Comm/TcpServerConfigObject.cs @@ -4,57 +4,56 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities +/// +public class TcpServerConfigObject { /// - /// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities + /// Uique key /// - public class TcpServerConfigObject - { - /// - /// Uique key - /// - public string Key { get; set; } - /// - /// Max Clients that the server will allow to connect. - /// - public ushort MaxClients { get; set; } - /// - /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic - /// - public bool Secure { get; set; } - /// - /// Port for the server to listen on - /// - public int Port { get; set; } - /// - /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client - /// - public bool SharedKeyRequired { get; set; } - /// - /// The shared key that must match on the server and client - /// - public string SharedKey { get; set; } - /// - /// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received. - /// heartbeats do not raise received events. - /// - public bool HeartbeatRequired { get; set; } - /// - /// The interval in seconds for the heartbeat from the client. If not received client is disconnected - /// - public ushort HeartbeatRequiredIntervalInSeconds { get; set; } - /// - /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided. - /// - public string HeartbeatStringToMatch { get; set; } - /// - /// Client buffer size. See Crestron help. defaults to 2000 if not greater than 2000 - /// - public int BufferSize { get; set; } - /// - /// Receive Queue size must be greater than 20 or defaults to 20 - /// - public int ReceiveQueueSize { get; set; } - } + public string Key { get; set; } + /// + /// Max Clients that the server will allow to connect. + /// + public ushort MaxClients { get; set; } + /// + /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic + /// + public bool Secure { get; set; } + /// + /// Port for the server to listen on + /// + public int Port { get; set; } + /// + /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client + /// + public bool SharedKeyRequired { get; set; } + /// + /// The shared key that must match on the server and client + /// + public string SharedKey { get; set; } + /// + /// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received. + /// heartbeats do not raise received events. + /// + public bool HeartbeatRequired { get; set; } + /// + /// The interval in seconds for the heartbeat from the client. If not received client is disconnected + /// + public ushort HeartbeatRequiredIntervalInSeconds { get; set; } + /// + /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided. + /// + public string HeartbeatStringToMatch { get; set; } + /// + /// Client buffer size. See Crestron help. defaults to 2000 if not greater than 2000 + /// + public int BufferSize { get; set; } + /// + /// Receive Queue size must be greater than 20 or defaults to 20 + /// + public int ReceiveQueueSize { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/eControlMethods.cs b/src/PepperDash.Core/Comm/eControlMethods.cs index b807fdc5..db1bcb19 100644 --- a/src/PepperDash.Core/Comm/eControlMethods.cs +++ b/src/PepperDash.Core/Comm/eControlMethods.cs @@ -4,84 +4,83 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Crestron Control Methods for a comm object +/// +public enum eControlMethod { /// - /// Crestron Control Methods for a comm object + /// /// - public enum eControlMethod - { - /// - /// - /// - None = 0, - /// - /// RS232/422/485 - /// - Com, - /// - /// Crestron IpId (most Crestron ethernet devices) - /// - IpId, - /// - /// Crestron IpIdTcp (HD-MD series, etc.) - /// - IpidTcp, - /// - /// Crestron IR control - /// - IR, - /// - /// SSH client - /// - Ssh, - /// - /// TCP/IP client - /// - Tcpip, - /// - /// Telnet - /// - Telnet, - /// - /// Crestnet device - /// - Cresnet, - /// - /// CEC Control, via a DM HDMI port - /// - Cec, - /// - /// UDP Server - /// - Udp, - /// - /// HTTP client - /// - Http, - /// - /// HTTPS client - /// - Https, - /// - /// Websocket client - /// - Ws, - /// - /// Secure Websocket client - /// - Wss, - /// - /// Secure TCP/IP - /// - SecureTcpIp, - /// - /// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction - /// - ComBridge, - /// - /// InfinetEX control - /// - InfinetEx - } + None = 0, + /// + /// RS232/422/485 + /// + Com, + /// + /// Crestron IpId (most Crestron ethernet devices) + /// + IpId, + /// + /// Crestron IpIdTcp (HD-MD series, etc.) + /// + IpidTcp, + /// + /// Crestron IR control + /// + IR, + /// + /// SSH client + /// + Ssh, + /// + /// TCP/IP client + /// + Tcpip, + /// + /// Telnet + /// + Telnet, + /// + /// Crestnet device + /// + Cresnet, + /// + /// CEC Control, via a DM HDMI port + /// + Cec, + /// + /// UDP Server + /// + Udp, + /// + /// HTTP client + /// + Http, + /// + /// HTTPS client + /// + Https, + /// + /// Websocket client + /// + Ws, + /// + /// Secure Websocket client + /// + Wss, + /// + /// Secure TCP/IP + /// + SecureTcpIp, + /// + /// Crestron COM bridge + /// + ComBridge, + /// + /// Crestron Infinet EX device + /// + InfinetEx } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs b/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs deleted file mode 100644 index f9f7eb3f..00000000 --- a/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 1fcce75b..04e36982 100644 --- a/src/PepperDash.Core/CommunicationExtras.cs +++ b/src/PepperDash.Core/CommunicationExtras.cs @@ -2,139 +2,142 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// An incoming communication stream +/// +public interface ICommunicationReceiver : IKeyed { /// - /// An incoming communication stream + /// Notifies of bytes received /// - public interface ICommunicationReceiver : IKeyed - { - /// - /// Notifies of bytes received - /// - event EventHandler BytesReceived; - /// - /// Notifies of text received - /// - event EventHandler TextReceived; - - /// - /// Indicates connection status - /// - [JsonProperty("isConnected")] - bool IsConnected { get; } - /// - /// Connect to the device - /// - void Connect(); - /// - /// Disconnect from the device - /// - void Disconnect(); - } + event EventHandler BytesReceived; + /// + /// Notifies of text received + /// + event EventHandler TextReceived; /// - /// Defines the contract for IBasicCommunication + /// Indicates connection status /// - public interface IBasicCommunication : ICommunicationReceiver - { - /// - /// Send text to the device - /// - /// + [JsonProperty("isConnected")] + bool IsConnected { get; } + /// + /// Connect to the device + /// + void Connect(); + /// + /// Disconnect from the device + /// + void Disconnect(); +} + + /// + /// Extends with methods for sending text and bytes to a device. + /// +public interface IBasicCommunication : ICommunicationReceiver + { + /// + /// Send text to the device + /// + /// void SendText(string text); - /// - /// Send bytes to the device - /// - /// + /// + /// Send bytes to the device + /// + /// void SendBytes(byte[] bytes); } - /// - /// Represents a device that implements IBasicCommunication and IStreamDebugging - /// - public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging - { +/// +/// Represents a device that implements IBasicCommunication and IStreamDebugging +/// +public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging +{ - } +} +/// +/// Represents a device with stream debugging capablities +/// +public interface IStreamDebugging : IKeyed +{ /// - /// Represents a device with stream debugging capablities + /// Object to enable stream debugging /// - public interface IStreamDebugging : IKeyed - { - /// - /// Object to enable stream debugging - /// - [JsonProperty("streamDebugging")] - CommunicationStreamDebugging StreamDebugging { get; } - } + [JsonProperty("streamDebugging")] + CommunicationStreamDebugging StreamDebugging { get; } +} + /// + /// For IBasicCommunication classes that have SocketStatus. GenericSshClient, + /// GenericTcpIpClient + /// + public interface ISocketStatus : IBasicCommunication + { /// - /// For IBasicCommunication classes that have SocketStatus. GenericSshClient, - /// GenericTcpIpClient + /// Notifies of socket status changes /// - public interface ISocketStatus : IBasicCommunication - { - /// - /// Notifies of socket status changes - /// event EventHandler ConnectionChange; - /// - /// The current socket status of the client - /// - [JsonProperty("clientStatus")] - [JsonConverter(typeof(StringEnumConverter))] - SocketStatus ClientStatus { get; } - } - /// - /// Describes a device that implements ISocketStatus and IStreamDebugging + /// The current socket status of the client /// - public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging - { + [JsonProperty("clientStatus")] + [JsonConverter(typeof(StringEnumConverter))] + SocketStatus ClientStatus { get; } + } - } +/// +/// Describes a device that implements ISocketStatus and IStreamDebugging +/// +public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging +{ - /// - /// Describes a device that can automatically attempt to reconnect - /// +} + +/// +/// Describes a device that can automatically attempt to reconnect +/// public interface IAutoReconnect - { - /// - /// Enable automatic recconnect - /// - [JsonProperty("autoReconnect")] - bool AutoReconnect { get; set; } - /// - /// Interval in ms to attempt automatic recconnections - /// - [JsonProperty("autoReconnectIntervalMs")] - int AutoReconnectIntervalMs { get; set; } - } - + { /// - /// + /// Enable automatic recconnect + /// + [JsonProperty("autoReconnect")] + bool AutoReconnect { get; set; } + /// + /// Interval in ms to attempt automatic recconnections + /// + [JsonProperty("autoReconnectIntervalMs")] + int AutoReconnectIntervalMs { get; set; } + } + + /// + /// + /// + public enum eGenericCommMethodStatusChangeType + { + /// + /// Connected /// - public enum eGenericCommMethodStatusChangeType - { - /// - /// Connected - /// Connected, - /// - /// Disconnected - /// - Disconnected - } + /// + /// Disconnected + /// + Disconnected + } /// /// This delegate defines handler for IBasicCommunication status changes @@ -143,20 +146,20 @@ namespace PepperDash.Core /// public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status); + /// + /// Event args for bytes received from a communication method + /// + public class GenericCommMethodReceiveBytesArgs : EventArgs + { /// - /// + /// The bytes received /// - public class GenericCommMethodReceiveBytesArgs : EventArgs - { - /// - /// Gets or sets the Bytes - /// - public byte[] Bytes { get; private set; } + public byte[] Bytes { get; private set; } - /// - /// - /// - /// + /// + /// Constructor + /// + /// public GenericCommMethodReceiveBytesArgs(byte[] bytes) { Bytes = bytes; @@ -168,42 +171,81 @@ namespace PepperDash.Core public GenericCommMethodReceiveBytesArgs() { } } + /// + /// Event args for text received + /// + public class GenericCommMethodReceiveTextArgs : EventArgs + { /// - /// + /// The text received /// - public class GenericCommMethodReceiveTextArgs : EventArgs - { - /// - /// - /// public string Text { get; private set; } - /// - /// - /// - public string Delimiter { get; private set; } - /// - /// - /// - /// + /// + /// The delimiter used to determine the end of a message, if applicable + /// + public string Delimiter { get; private set; } + + /// + /// Constructor + /// + /// public GenericCommMethodReceiveTextArgs(string text) { Text = text; } - /// - /// - /// - /// - /// - public GenericCommMethodReceiveTextArgs(string text, string delimiter) - : this(text) - { - Delimiter = delimiter; - } - - /// - /// S+ Constructor - /// - public GenericCommMethodReceiveTextArgs() { } + /// + /// + /// + /// + /// + public GenericCommMethodReceiveTextArgs(string text, string delimiter) + :this(text) + { + Delimiter = delimiter; } -} \ No newline at end of file + + /// + /// S+ Constructor + /// + public GenericCommMethodReceiveTextArgs() { } + } + + + + /// + /// Helper class to get escaped text for debugging communication streams + /// + public class ComTextHelper + { + /// + /// Gets escaped text for a byte array + /// + /// + /// + 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 + /// + /// + /// + 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 + /// + /// + /// + public static string GetDebugText(string text) + { + return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value)); + } + } diff --git a/src/PepperDash.Core/Config/PortalConfigReader.cs b/src/PepperDash.Core/Config/PortalConfigReader.cs index b7c5c784..270e10ec 100644 --- a/src/PepperDash.Core/Config/PortalConfigReader.cs +++ b/src/PepperDash.Core/Config/PortalConfigReader.cs @@ -11,15 +11,11 @@ using JToken = NewtonsoftJson::Newtonsoft.Json.Linq.JToken; using PepperDash.Core; using Serilog.Events; +namespace PepperDash.Core.Config; - -namespace PepperDash.Core.Config -{ - - - /// - /// Reads a Portal formatted config file - /// +/// +/// Reads a Portal formatted config file +/// public class PortalConfigReader { const string template = "template"; @@ -128,31 +124,31 @@ namespace PepperDash.Core.Config Merge(template[destinationLists], system[destinationLists], destinationLists)); - if (system[cameraLists] == null) - merged.Add(cameraLists, template[cameraLists]); - else - merged.Add(cameraLists, Merge(template[cameraLists], system[cameraLists], cameraLists)); + if (system["cameraLists"] == null) + merged.Add("cameraLists", template["cameraLists"]); + else + merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists")); - if (system[audioControlPointLists] == null) - merged.Add(audioControlPointLists, template[audioControlPointLists]); - else - merged.Add(audioControlPointLists, - Merge(template[audioControlPointLists], system[audioControlPointLists], audioControlPointLists)); + if (system["audioControlPointLists"] == null) + merged.Add("audioControlPointLists", template["audioControlPointLists"]); + else + 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]); + // 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"]); else merged.Add(tieLines, new JArray()); - if (template[joinMaps] != null) - merged.Add(joinMaps, template[joinMaps]); - else - merged.Add(joinMaps, new JObject()); + if (template["joinMaps"] != null) + merged.Add("joinMaps", template["joinMaps"]); + else + merged.Add("joinMaps", new JObject()); if (system[global] != null) merged.Add(global, Merge(template[global], system[global], global)); @@ -175,26 +171,26 @@ namespace PepperDash.Core.Config return a1; else if (a1 != null) { - if (a2[0]["key"] == null) // If the first item in the system array has no key, overwrite the template array - { // with the system array - return a2; - } - else // The arrays are keyed, merge them by key + if (a2[0]["key"] == null) // If the first item in the system array has no key, overwrite the template array + { // with the system array + return a2; + } + else // The arrays are keyed, merge them by key + { + for (int i = 0; i < a1.Count(); i++) { - for (int i = 0; i < a1.Count(); i++) + var a1Dev = a1[i]; + // Try to get a system device and if found, merge it onto template + var a2Match = a2.FirstOrDefault(t => t[propertyName].Equals(a1Dev[propertyName]));// t.Value("uid") == tmplDev.Value("uid")); + if (a2Match != null) { - var a1Dev = a1[i]; - // Try to get a system device and if found, merge it onto template - var a2Match = a2.FirstOrDefault(t => t[propertyName].Equals(a1Dev[propertyName]));// t.Value("uid") == tmplDev.Value("uid")); - if (a2Match != null) - { - var mergedItem = Merge(a1Dev, a2Match, string.Format("{0}[{1}].", path, i));// Merge(JObject.FromObject(a1Dev), JObject.FromObject(a2Match)); - result.Add(mergedItem); - } - else - result.Add(a1Dev); + var mergedItem = Merge(a1Dev, a2Match, string.Format("{0}[{1}].", path, i));// Merge(JObject.FromObject(a1Dev), JObject.FromObject(a2Match)); + result.Add(mergedItem); } + else + result.Add(a1Dev); } + } } return result; } @@ -211,9 +207,9 @@ namespace PepperDash.Core.Config /// /// Merge o2 onto o1 /// - /// - /// - /// + /// + /// + /// static JObject Merge(JObject o1, JObject o2, string path) { foreach (var o2Prop in o2) @@ -259,5 +255,4 @@ namespace PepperDash.Core.Config } return o1; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/Conversion/Convert.cs b/src/PepperDash.Core/Conversion/Convert.cs index 49f8bb94..13ef7263 100644 --- a/src/PepperDash.Core/Conversion/Convert.cs +++ b/src/PepperDash.Core/Conversion/Convert.cs @@ -4,28 +4,18 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core +namespace PepperDash.Core; + +public class EncodingHelper { - /// - /// Represents a EncodingHelper - /// - public class EncodingHelper + public static string ConvertUtf8ToAscii(string utf8String) { - /// - /// ConvertUtf8ToAscii method - /// - public static string ConvertUtf8ToAscii(string utf8String) - { - return Encoding.ASCII.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length); - } - - /// - /// ConvertUtf8ToUtf16 method - /// - public static string ConvertUtf8ToUtf16(string utf8String) - { - return Encoding.Unicode.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length); - } - + return Encoding.ASCII.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length); } + + public static string ConvertUtf8ToUtf16(string utf8String) + { + return Encoding.Unicode.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length); + } + } \ No newline at end of file diff --git a/src/PepperDash.Core/CoreInterfaces.cs b/src/PepperDash.Core/CoreInterfaces.cs index 7eb565dd..89e6cdad 100644 --- a/src/PepperDash.Core/CoreInterfaces.cs +++ b/src/PepperDash.Core/CoreInterfaces.cs @@ -8,30 +8,28 @@ using Crestron.SimplSharp; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using Serilog; -namespace PepperDash.Core -{ - /// - /// Unique key interface to require a unique key for the class - /// +namespace PepperDash.Core; + +/// +/// Unique key interface to require a unique key for the class +/// public interface IKeyed { - /// - /// Gets the unique key associated with the object. - /// - [JsonProperty("key")] - string Key { get; } - } - - /// - /// Named Keyed device interface. Forces the device to have a Unique Key and a name. + /// + /// Gets the unique key associated with the object. /// - public interface IKeyName : IKeyed - { - /// - /// Gets the name associated with the current object. - /// - [JsonProperty("name")] - string Name { get; } - } + [JsonProperty("key")] + string Key { get; } +} +/// +/// Named Keyed device interface. Forces the device to have a Unique Key and a name. +/// + public interface IKeyName : IKeyed +{ + /// + /// Gets the name associated with the current object. + /// + [JsonProperty("name")] + string Name { get; } } \ No newline at end of file diff --git a/src/PepperDash.Core/Device.cs b/src/PepperDash.Core/Device.cs index 7124550e..70d4d97f 100644 --- a/src/PepperDash.Core/Device.cs +++ b/src/PepperDash.Core/Device.cs @@ -2,198 +2,194 @@ using System.Collections.Generic; using Serilog.Events; -namespace PepperDash.Core +namespace PepperDash.Core; + +//********************************************************************************************************* +/// +/// Represents a Device +/// +public class Device : IKeyName { - //********************************************************************************************************* - /// - /// Represents a Device - /// - public class Device : IKeyName + + /// + /// Unique Key + /// + public string Key { get; protected set; } + /// + /// Gets or sets the Name + /// + public string Name { get; protected set; } + /// + /// + /// + public bool Enabled { get; protected set; } + + /// + /// A place to store reference to the original config object, if any. These values should + /// NOT be used as properties on the device as they are all publicly-settable values. + /// + //public DeviceConfig Config { get; private set; } + /// + /// Helper method to check if Config exists + /// + //public bool HasConfig { get { return Config != null; } } + + List _PreActivationActions; + List _PostActivationActions; + + /// + /// + /// + public static Device DefaultDevice { get { return _DefaultDevice; } } + static Device _DefaultDevice = new Device("Default", "Default"); + + /// + /// Base constructor for all Devices. + /// + /// + public Device(string key) { + Key = key; + if (key.Contains(".")) Debug.LogMessage(LogEventLevel.Information, "WARNING: Device key should not include '.'", this); + Name = ""; + } - /// - /// Unique Key - /// - public string Key { get; protected set; } - /// - /// Gets or sets the Name - /// - public string Name { get; protected set; } - /// - /// - /// - public bool Enabled { get; protected set; } + /// + /// Constructor with key and name + /// + /// + /// + public Device(string key, string name) : this(key) + { + Name = name; - /// - /// A place to store reference to the original config object, if any. These values should - /// NOT be used as properties on the device as they are all publicly-settable values. - /// - //public DeviceConfig Config { get; private set; } - /// - /// Helper method to check if Config exists - /// - //public bool HasConfig { get { return Config != null; } } + } - List _PreActivationActions; - List _PostActivationActions; + //public Device(DeviceConfig config) + // : this(config.Key, config.Name) + //{ + // Config = config; + //} - /// - /// - /// - public static Device DefaultDevice { get { return _DefaultDevice; } } - static Device _DefaultDevice = new Device("Default", "Default"); + /// + /// Adds a pre activation action + /// + /// + public void AddPreActivationAction(Action act) + { + if (_PreActivationActions == null) + _PreActivationActions = new List(); + _PreActivationActions.Add(act); + } - /// - /// Base constructor for all Devices. - /// - /// - public Device(string key) - { - Key = key; - if (key.Contains(".")) Debug.LogMessage(LogEventLevel.Information, "WARNING: Device key should not include '.'", this); - Name = ""; - } + /// + /// Adds a post activation action + /// + /// + /// + /// AddPostActivationAction method + /// + public void AddPostActivationAction(Action act) + { + if (_PostActivationActions == null) + _PostActivationActions = new List(); + _PostActivationActions.Add(act); + } - /// - /// Constructor with key and name - /// - /// - /// - public Device(string key, string name) : this(key) - { - Name = name; - - } - - //public Device(DeviceConfig config) - // : this(config.Key, config.Name) - //{ - // Config = config; - //} - - /// - /// Adds a pre activation action - /// - /// - public void AddPreActivationAction(Action act) - { - if (_PreActivationActions == null) - _PreActivationActions = new List(); - _PreActivationActions.Add(act); - } - - /// - /// Adds a post activation action - /// - /// - /// - /// AddPostActivationAction method - /// - public void AddPostActivationAction(Action act) - { - if (_PostActivationActions == null) - _PostActivationActions = new List(); - _PostActivationActions.Add(act); - } - - /// - /// PreActivate method - /// - public void PreActivate() - { - if (_PreActivationActions != null) - _PreActivationActions.ForEach(a => + /// + /// PreActivate method + /// + public void PreActivate() + { + if (_PreActivationActions != null) + _PreActivationActions.ForEach(a => + { + try { - try - { - a.Invoke(); - } - catch (Exception e) - { - Debug.LogMessage(e, "Error in PreActivationAction: " + e.Message, this); - } - }); - } - - /// - /// Activate method - /// - public bool Activate() - { - //if (_PreActivationActions != null) - // _PreActivationActions.ForEach(a => a.Invoke()); - var result = CustomActivate(); - //if(result && _PostActivationActions != null) - // _PostActivationActions.ForEach(a => a.Invoke()); - return result; - } - - /// - /// PostActivate method - /// - public void PostActivate() - { - if (_PostActivationActions != null) - _PostActivationActions.ForEach(a => + a.Invoke(); + } + catch (Exception e) { - try - { - a.Invoke(); - } - catch (Exception e) - { - Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this); - } - }); - } + Debug.LogMessage(e, "Error in PreActivationAction: " + e.Message, this); + } + }); + } - /// - /// Called in between Pre and PostActivationActions when Activate() is called. - /// Override to provide addtitional setup when calling activation. Overriding classes - /// do not need to call base.CustomActivate() - /// - /// true if device activated successfully. - /// - /// CustomActivate method - /// - public virtual bool CustomActivate() { return true; } + /// + /// Activate method + /// + public bool Activate() + { + //if (_PreActivationActions != null) + // _PreActivationActions.ForEach(a => a.Invoke()); + var result = CustomActivate(); + //if(result && _PostActivationActions != null) + // _PostActivationActions.ForEach(a => a.Invoke()); + return result; + } - /// - /// Call to deactivate device - unlink events, etc. Overriding classes do not - /// need to call base.Deactivate() - /// - /// - public virtual bool Deactivate() { return true; } + /// + /// PostActivate method + /// + public void PostActivate() + { + if (_PostActivationActions != null) + _PostActivationActions.ForEach(a => + { + try + { + a.Invoke(); + } + catch (Exception e) + { + Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this); + } + }); + } - /// - /// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize() - /// - public virtual void Initialize() - { - } + /// + /// Called in between Pre and PostActivationActions when Activate() is called. + /// Override to provide addtitional setup when calling activation. Overriding classes + /// do not need to call base.CustomActivate() + /// + /// true if device activated successfully. + /// + /// CustomActivate method + /// + public virtual bool CustomActivate() { return true; } - /// - /// Helper method to check object for bool value false and fire an Action method - /// - /// Should be of type bool, others will be ignored - /// Action to be run when o is false - public void OnFalse(object o, Action a) - { - if (o is bool && !(bool)o) a(); - } + /// + /// Call to deactivate device - unlink events, etc. Overriding classes do not + /// need to call base.Deactivate() + /// + /// + public virtual bool Deactivate() { return true; } - /// - /// Returns a string representation of the object, including its key and name. - /// - /// The returned string is formatted as "{Key} - {Name}". If the Name property is - /// null or empty, "---" is used in place of the name. - /// A string that represents the object, containing the key and name in the format "{Key} - {Name}". - /// - /// ToString method - /// - public override string ToString() - { - return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name); - } + /// + /// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize() + /// + public virtual void Initialize() + { + } + + /// + /// Helper method to check object for bool value false and fire an Action method + /// + /// Should be of type bool, others will be ignored + /// Action to be run when o is false + public void OnFalse(object o, Action a) + { + if (o is bool && !(bool)o) a(); + } + + /// + /// Returns a string representation of the object, including its key and name. + /// + /// The returned string is formatted as "{Key} - {Name}". If the Name property is + /// null or empty, "---" is used in place of the name. + /// A string that represents the object, containing the key and name in the format "{Key} - {Name}". + public override string ToString() + { + return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name); } } \ No newline at end of file diff --git a/src/PepperDash.Core/EthernetHelper.cs b/src/PepperDash.Core/EthernetHelper.cs index 20709eb1..51372603 100644 --- a/src/PepperDash.Core/EthernetHelper.cs +++ b/src/PepperDash.Core/EthernetHelper.cs @@ -4,11 +4,11 @@ using Crestron.SimplSharp; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using Serilog.Events; -namespace PepperDash.Core -{ - /// - /// Represents a EthernetHelper - /// +namespace PepperDash.Core; + +/// +/// Represents an EthernetHelper. +/// public class EthernetHelper { /// @@ -115,5 +115,4 @@ namespace PepperDash.Core CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, 0); } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/EventArgs.cs b/src/PepperDash.Core/EventArgs.cs index c146a6f2..68c2be53 100644 --- a/src/PepperDash.Core/EventArgs.cs +++ b/src/PepperDash.Core/EventArgs.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core -{ +namespace PepperDash.Core; + /// /// Bool change event args /// @@ -168,5 +168,4 @@ namespace PepperDash.Core Type = type; Index = index; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs b/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs index 1b78c33f..d8a09e4e 100644 --- a/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs +++ b/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs @@ -4,13 +4,13 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.GenericRESTfulCommunications -{ +namespace PepperDash.Core.GenericRESTfulCommunications; + /// /// Constants /// - public class GenericRESTfulConstants - { +public class GenericRESTfulConstants +{ /// /// Generic boolean change /// @@ -35,5 +35,4 @@ namespace PepperDash.Core.GenericRESTfulCommunications /// Error string change /// public const ushort ErrorStringChange = 203; - } } \ No newline at end of file diff --git a/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs b/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs index bd33e13c..16a2ee04 100644 --- a/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs +++ b/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs @@ -6,8 +6,8 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.Net.Http; using Crestron.SimplSharp.Net.Https; -namespace PepperDash.Core.GenericRESTfulCommunications -{ +namespace PepperDash.Core.GenericRESTfulCommunications; + /// /// Generic RESTful communication class /// @@ -42,7 +42,7 @@ namespace PepperDash.Core.GenericRESTfulCommunications /// /// /// - /// + /// public void SubmitRequest(string url, ushort port, ushort requestType, string contentType, string username, string password) { if (url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)) @@ -65,7 +65,7 @@ namespace PepperDash.Core.GenericRESTfulCommunications /// /// /// - /// + /// /// /// private void SubmitRequestHttp(string url, ushort port, ushort requestType, string contentType, string username, string password) @@ -123,7 +123,7 @@ namespace PepperDash.Core.GenericRESTfulCommunications /// /// /// - /// + /// /// /// private void SubmitRequestHttps(string url, ushort port, ushort requestType, string contentType, string username, string password) @@ -252,5 +252,4 @@ namespace PepperDash.Core.GenericRESTfulCommunications StringChange(this, args); } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonStandardObjects/EventArgs and Constants.cs b/src/PepperDash.Core/JsonStandardObjects/EventArgs and Constants.cs index 110587ab..668bd481 100644 --- a/src/PepperDash.Core/JsonStandardObjects/EventArgs and Constants.cs +++ b/src/PepperDash.Core/JsonStandardObjects/EventArgs and Constants.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.JsonStandardObjects -{ +namespace PepperDash.Core.JsonStandardObjects; + /// /// Constants for simpl modules /// @@ -73,5 +73,4 @@ namespace PepperDash.Core.JsonStandardObjects Type = type; Index = index; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDevice.cs b/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDevice.cs index 2ad47e27..621e99e0 100644 --- a/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDevice.cs +++ b/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDevice.cs @@ -4,8 +4,8 @@ using Crestron.SimplSharp; using PepperDash.Core.JsonToSimpl; using Serilog.Events; -namespace PepperDash.Core.JsonStandardObjects -{ +namespace PepperDash.Core.JsonStandardObjects; + /// /// Device class /// @@ -182,5 +182,4 @@ namespace PepperDash.Core.JsonStandardObjects } #endregion EventHandler Helpers - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDeviceConfig.cs b/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDeviceConfig.cs index d754a029..5d98ba5a 100644 --- a/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDeviceConfig.cs +++ b/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDeviceConfig.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.JsonStandardObjects -{ +namespace PepperDash.Core.JsonStandardObjects; + /* Convert JSON snippt to C#: http://json2csharp.com/# @@ -52,55 +52,55 @@ namespace PepperDash.Core.JsonStandardObjects /// public class ComParamsConfig { - /// - /// Gets or sets the baudRate - /// + /// + /// + /// public int baudRate { get; set; } - /// - /// - /// + /// + /// + /// public int dataBits { get; set; } - /// - /// - /// + /// + /// + /// public int stopBits { get; set; } - /// - /// - /// + /// + /// + /// public string parity { get; set; } - /// - /// - /// + /// + /// + /// public string protocol { get; set; } - /// - /// - /// + /// + /// + /// public string hardwareHandshake { get; set; } - /// - /// - /// + /// + /// + /// public string softwareHandshake { get; set; } - /// - /// - /// + /// + /// + /// public int pacing { get; set; } // convert properties for simpl - /// - /// Gets or sets the simplBaudRate - /// + /// + /// + /// public ushort simplBaudRate { get { return Convert.ToUInt16(baudRate); } } - /// - /// Gets or sets the simplDataBits - /// + /// + /// + /// public ushort simplDataBits { get { return Convert.ToUInt16(dataBits); } } - /// - /// - /// + /// + /// + /// public ushort simplStopBits { get { return Convert.ToUInt16(stopBits); } } - /// - /// - /// + /// + /// + /// public ushort simplPacing { get { return Convert.ToUInt16(pacing); } } /// @@ -117,43 +117,43 @@ namespace PepperDash.Core.JsonStandardObjects /// public class TcpSshPropertiesConfig { - /// - /// - /// + /// + /// + /// public string address { get; set; } - /// - /// - /// + /// + /// + /// public int port { get; set; } - /// - /// - /// + /// + /// + /// public string username { get; set; } - /// - /// - /// + /// + /// + /// public string password { get; set; } - /// - /// - /// + /// + /// + /// public bool autoReconnect { get; set; } - /// - /// - /// + /// + /// + /// public int autoReconnectIntervalMs { get; set; } // convert properties for simpl - /// - /// Gets or sets the simplPort - /// + /// + /// + /// public ushort simplPort { get { return Convert.ToUInt16(port); } } - /// - /// Gets or sets the simplAutoReconnect - /// + /// + /// + /// public ushort simplAutoReconnect { get { return (ushort)(autoReconnect ? 1 : 0); } } - /// - /// - /// + /// + /// + /// public ushort simplAutoReconnectIntervalMs { get { return Convert.ToUInt16(autoReconnectIntervalMs); } } /// @@ -170,31 +170,31 @@ namespace PepperDash.Core.JsonStandardObjects /// public class ControlConfig { - /// - /// - /// + /// + /// + /// public string method { get; set; } - /// - /// - /// + /// + /// + /// public string controlPortDevKey { get; set; } - /// - /// - /// + /// + /// + /// public int controlPortNumber { get; set; } - /// - /// - /// + /// + /// + /// public ComParamsConfig comParams { get; set; } - /// - /// - /// + /// + /// + /// public TcpSshPropertiesConfig tcpSshProperties { get; set; } // convert properties for simpl - /// - /// Gets or sets the simplControlPortNumber - /// + /// + /// + /// public ushort simplControlPortNumber { get { return Convert.ToUInt16(controlPortNumber); } } /// @@ -212,27 +212,27 @@ namespace PepperDash.Core.JsonStandardObjects /// public class PropertiesConfig { - /// - /// - /// + /// + /// + /// public int deviceId { get; set; } - /// - /// - /// + /// + /// + /// public bool enabled { get; set; } - /// - /// - /// + /// + /// + /// public ControlConfig control { get; set; } // convert properties for simpl - /// - /// Gets or sets the simplDeviceId - /// + /// + /// + /// public ushort simplDeviceId { get { return Convert.ToUInt16(deviceId); } } - /// - /// Gets or sets the simplEnabled - /// + /// + /// + /// public ushort simplEnabled { get { return (ushort)(enabled ? 1 : 0); } } /// @@ -249,9 +249,8 @@ namespace PepperDash.Core.JsonStandardObjects /// public class RootObject { - /// - /// The collection of devices - /// + /// + /// The collection of devices + /// public List devices { get; set; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonToSimpl/Constants.cs b/src/PepperDash.Core/JsonToSimpl/Constants.cs index ca638bbf..a04c1966 100644 --- a/src/PepperDash.Core/JsonToSimpl/Constants.cs +++ b/src/PepperDash.Core/JsonToSimpl/Constants.cs @@ -4,78 +4,78 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.JsonToSimpl -{ +namespace PepperDash.Core.JsonToSimpl; + /// /// Constants for Simpl modules /// public class JsonToSimplConstants { - /// - /// - /// + /// + /// + /// public const ushort BoolValueChange = 1; - /// - /// - /// + /// + /// + /// public const ushort JsonIsValidBoolChange = 2; - /// - /// Reports the if the device is 3-series compatible - /// - public const ushort ProgramCompatibility3SeriesChange = 3; + /// + /// Reports the if the device is 3-series compatible + /// + public const ushort ProgramCompatibility3SeriesChange = 3; - /// - /// Reports the if the device is 4-series compatible - /// - public const ushort ProgramCompatibility4SeriesChange = 4; + /// + /// Reports the if the device is 4-series compatible + /// + public const ushort ProgramCompatibility4SeriesChange = 4; - /// - /// Reports the device platform enum value - /// - public const ushort DevicePlatformValueChange = 5; + /// + /// Reports the device platform enum value + /// + public const ushort DevicePlatformValueChange = 5; - /// - /// - /// + /// + /// + /// public const ushort UshortValueChange = 101; - /// - /// - /// + /// + /// + /// public const ushort StringValueChange = 201; - /// - /// - /// + /// + /// + /// public const ushort FullPathToArrayChange = 202; - /// - /// - /// + /// + /// + /// public const ushort ActualFilePathChange = 203; - /// - /// - /// + /// + /// + /// public const ushort FilenameResolvedChange = 204; - /// - /// - /// + /// + /// + /// public const ushort FilePathResolvedChange = 205; - /// - /// Reports the root directory change - /// - public const ushort RootDirectoryChange = 206; + /// + /// Reports the root directory change + /// + public const ushort RootDirectoryChange = 206; - /// - /// Reports the room ID change - /// - public const ushort RoomIdChange = 207; + /// + /// Reports the room ID change + /// + public const ushort RoomIdChange = 207; - /// - /// Reports the room name change - /// - public const ushort RoomNameChange = 208; + /// + /// Reports the room name change + /// + public const ushort RoomNameChange = 208; } /// @@ -88,33 +88,33 @@ namespace PepperDash.Core.JsonToSimpl /// public class SPlusValueWrapper { - /// - /// Gets or sets the ValueType - /// + /// + /// + /// public SPlusType ValueType { get; private set; } - /// - /// - /// + /// + /// + /// public ushort Index { get; private set; } - /// - /// - /// + /// + /// + /// public ushort BoolUShortValue { get; set; } - /// - /// - /// + /// + /// + /// public string StringValue { get; set; } - /// - /// - /// + /// + /// + /// public SPlusValueWrapper() {} - /// - /// - /// - /// - /// + /// + /// + /// + /// + /// public SPlusValueWrapper(SPlusType type, ushort index) { ValueType = type; @@ -127,17 +127,16 @@ namespace PepperDash.Core.JsonToSimpl /// public enum SPlusType { - /// - /// Digital - /// + /// + /// Digital + /// Digital, - /// - /// Analog - /// - Analog, - /// - /// String - /// - String - } -} \ No newline at end of file + /// + /// Analog + /// + Analog, + /// + /// String + /// + String + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonToSimpl/Global.cs b/src/PepperDash.Core/JsonToSimpl/Global.cs index 8d71c5fb..39633a61 100644 --- a/src/PepperDash.Core/JsonToSimpl/Global.cs +++ b/src/PepperDash.Core/JsonToSimpl/Global.cs @@ -7,11 +7,11 @@ using Serilog.Events; //using PepperDash.Core; -namespace PepperDash.Core.JsonToSimpl -{ - /// - /// The global class to manage all the instances of JsonToSimplMaster - /// +namespace PepperDash.Core.JsonToSimpl; + +/// +/// The global class to manage all the instances of JsonToSimplMaster +/// public class J2SGlobal { static List Masters = new List(); @@ -22,10 +22,7 @@ namespace PepperDash.Core.JsonToSimpl /// master, this will fail /// /// New master to add - /// - /// - /// AddMaster method - /// + /// public static void AddMaster(JsonToSimplMaster master) { if (master == null) @@ -59,5 +56,4 @@ namespace PepperDash.Core.JsonToSimpl { return Masters.FirstOrDefault(m => m.UniqueID.Equals(file, StringComparison.OrdinalIgnoreCase)); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs index 559fc87d..f49a4b77 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs @@ -5,20 +5,20 @@ using System.Linq; using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray; using Serilog.Events; -namespace PepperDash.Core.JsonToSimpl -{ - /// - /// Represents a JsonToSimplArrayLookupChild - /// +namespace PepperDash.Core.JsonToSimpl; + +/// +/// Used to interact with an array of values with the S+ modules +/// public class JsonToSimplArrayLookupChild : JsonToSimplChildObjectBase { - /// - /// - /// + /// + /// + /// public string SearchPropertyName { get; set; } - /// - /// - /// + /// + /// + /// public string SearchPropertyValue { get; set; } int ArrayIndex; @@ -78,10 +78,9 @@ namespace PepperDash.Core.JsonToSimpl PathSuffix == null ? "" : PathSuffix); } - /// - /// ProcessAll method - /// - /// + /// + /// Process all values + /// public override void ProcessAll() { if (FindInArray()) @@ -161,5 +160,4 @@ namespace PepperDash.Core.JsonToSimpl return false; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs index e7da9c9c..2c1afd06 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs @@ -5,29 +5,29 @@ using System.Collections.Generic; using System.Linq; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; -namespace PepperDash.Core.JsonToSimpl -{ - /// - /// Base class for JSON objects - /// +namespace PepperDash.Core.JsonToSimpl; + +/// +/// Base class for JSON objects +/// public abstract class JsonToSimplChildObjectBase : IKeyed { - /// - /// Notifies of bool change - /// + /// + /// Notifies of bool change + /// public event EventHandler BoolChange; - /// - /// Notifies of ushort change - /// + /// + /// Notifies of ushort change + /// public event EventHandler UShortChange; - /// - /// Notifies of string change - /// + /// + /// Notifies of string change + /// public event EventHandler StringChange; - /// - /// Delegate to get all values - /// + /// + /// Delegate to get all values + /// public SPlusValuesDelegate GetAllValuesDelegate { get; set; } /// @@ -35,9 +35,9 @@ namespace PepperDash.Core.JsonToSimpl /// public SPlusValuesDelegate SetAllPathsDelegate { get; set; } - /// - /// Gets or sets the Key - /// + /// + /// Unique identifier for instance + /// public string Key { get; protected set; } /// @@ -51,33 +51,33 @@ namespace PepperDash.Core.JsonToSimpl /// public string PathSuffix { get; protected set; } - /// - /// Gets or sets the LinkedToObject - /// + /// + /// Indicates if the instance is linked to an object + /// public bool LinkedToObject { get; protected set; } - /// - /// Reference to Master instance - /// + /// + /// Reference to Master instance + /// protected JsonToSimplMaster Master; - /// - /// Paths to boolean values in JSON structure - /// - protected Dictionary BoolPaths = new Dictionary(); - /// - /// Paths to numeric values in JSON structure - /// + /// + /// Paths to boolean values in JSON structure + /// + protected Dictionary BoolPaths = new Dictionary(); + /// + /// Paths to numeric values in JSON structure + /// protected Dictionary UshortPaths = new Dictionary(); - /// - /// Paths to string values in JSON structure - /// + /// + /// Paths to string values in JSON structure + /// protected Dictionary StringPaths = new Dictionary(); /// /// Call this before doing anything else /// - /// + /// /// /// /// @@ -94,13 +94,10 @@ namespace PepperDash.Core.JsonToSimpl Debug.Console(1, "JSON Child [{0}] cannot link to master {1}", key, masterUniqueId); } - /// - /// Sets the path prefix for the object - /// - /// - /// - /// SetPathPrefix method - /// + /// + /// Sets the path prefix for the object + /// + /// public void SetPathPrefix(string pathPrefix) { PathPrefix = pathPrefix; @@ -175,18 +172,18 @@ namespace PepperDash.Core.JsonToSimpl } // Processes the path to a ushort, converting to ushort if able, twos complement if necessary, firing off UshrtChange event - void ProcessUshortPath(ushort index) { - string response; - if (Process(UshortPaths[index], out response)) { - ushort val; - try { val = Convert.ToInt32(response) < 0 ? (ushort)(Convert.ToInt16(response) + 65536) : Convert.ToUInt16(response); } - catch { val = 0; } + void ProcessUshortPath(ushort index) { + string response; + if (Process(UshortPaths[index], out response)) { + ushort val; + try { val = Convert.ToInt32(response) < 0 ? (ushort)(Convert.ToInt16(response) + 65536) : Convert.ToUInt16(response); } + catch { val = 0; } - OnUShortChange(val, index, JsonToSimplConstants.UshortValueChange); - } - else { } - // OnUShortChange(0, index, JsonToSimplConstants.UshortValueChange); + OnUShortChange(val, index, JsonToSimplConstants.UshortValueChange); } + else { } + // OnUShortChange(0, index, JsonToSimplConstants.UshortValueChange); + } // Processes the path to a string property and fires of a StringChange event. void ProcessStringPath(ushort index) @@ -277,69 +274,54 @@ namespace PepperDash.Core.JsonToSimpl GetAllValuesDelegate(); } - /// - /// - /// - /// - /// - /// - /// USetBoolValue method - /// + /// + /// + /// + /// + /// public void USetBoolValue(ushort key, ushort theValue) { SetBoolValue(key, theValue == 1); } - /// - /// - /// - /// - /// - /// - /// SetBoolValue method - /// + /// + /// + /// + /// + /// public void SetBoolValue(ushort key, bool theValue) { if (BoolPaths.ContainsKey(key)) SetValueOnMaster(BoolPaths[key], new JValue(theValue)); } - /// - /// - /// - /// - /// - /// - /// SetUShortValue method - /// + /// + /// + /// + /// + /// public void SetUShortValue(ushort key, ushort theValue) { if (UshortPaths.ContainsKey(key)) SetValueOnMaster(UshortPaths[key], new JValue(theValue)); } - /// - /// - /// - /// - /// - /// - /// SetStringValue method - /// + /// + /// + /// + /// + /// public void SetStringValue(ushort key, string theValue) { if (StringPaths.ContainsKey(key)) SetValueOnMaster(StringPaths[key], new JValue(theValue)); } - /// - /// - /// - /// - /// - /// - /// SetValueOnMaster method - /// + /// + /// + /// + /// + /// public void SetValueOnMaster(string keyPath, JValue valueToSave) { var path = GetFullPath(keyPath); @@ -369,12 +351,12 @@ namespace PepperDash.Core.JsonToSimpl // Helpers for events //****************************************************************************************** - /// - /// Event helper - /// - /// - /// - /// + /// + /// Event helper + /// + /// + /// + /// protected void OnBoolChange(bool state, ushort index, ushort type) { var handler = BoolChange; @@ -387,12 +369,12 @@ namespace PepperDash.Core.JsonToSimpl } //****************************************************************************************** - /// - /// Event helper - /// - /// - /// - /// + /// + /// Event helper + /// + /// + /// + /// protected void OnUShortChange(ushort state, ushort index, ushort type) { var handler = UShortChange; @@ -404,12 +386,12 @@ namespace PepperDash.Core.JsonToSimpl } } - /// - /// Event helper - /// - /// - /// - /// + /// + /// Event helper + /// + /// + /// + /// protected void OnStringChange(string value, ushort index, ushort type) { var handler = StringChange; @@ -420,5 +402,4 @@ namespace PepperDash.Core.JsonToSimpl StringChange(this, args); } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs index bf44e2b2..a9fa03cd 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs @@ -11,284 +11,280 @@ using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; -namespace PepperDash.Core.JsonToSimpl +namespace PepperDash.Core.JsonToSimpl; + +/// +/// Represents a JSON file that can be read and written to +/// +public class JsonToSimplFileMaster : JsonToSimplMaster { /// - /// Represents a JSON file that can be read and written to + /// Sets the filepath as well as registers this with the Global.Masters list /// - public class JsonToSimplFileMaster : JsonToSimplMaster + public string Filepath { get; private set; } + + /// + /// Filepath to the actual file that will be read (Portal or local) + /// + public string ActualFilePath { get; private set; } + + /// + /// + /// + public string Filename { get; private set; } + /// + /// + /// + public string FilePathName { get; private set; } + + /*****************************************************************************************/ + /** Privates **/ + + + // The JSON file in JObject form + // For gathering the incoming data + object StringBuilderLock = new object(); + // To prevent multiple same-file access + static object FileLock = new object(); + + /*****************************************************************************************/ + + /// + /// SIMPL+ default constructor. + /// + public JsonToSimplFileMaster() { - /// - /// Sets the filepath as well as registers this with the Global.Masters list - /// - public string Filepath { get; private set; } + } - /// - /// Gets or sets the ActualFilePath - /// - public string ActualFilePath { get; private set; } - - /// - /// Gets or sets the Filename - /// - public string Filename { get; private set; } - /// - /// - /// - public string FilePathName { get; private set; } - - /*****************************************************************************************/ - /** Privates **/ - - - // The JSON file in JObject form - // For gathering the incoming data - object StringBuilderLock = new object(); - // To prevent multiple same-file access - static object FileLock = new object(); - - /*****************************************************************************************/ - - /// - /// SIMPL+ default constructor. - /// - public JsonToSimplFileMaster() + /// + /// Read, evaluate and udpate status + /// + public void EvaluateFile(string filepath) + { + try { - } + OnBoolChange(false, 0, JsonToSimplConstants.JsonIsValidBoolChange); - /// - /// Read, evaluate and udpate status - /// - public void EvaluateFile(string filepath) - { - try + var dirSeparator = Path.DirectorySeparatorChar; + var dirSeparatorAlt = Path.AltDirectorySeparatorChar; + + var series = CrestronEnvironment.ProgramCompatibility; + + var is3Series = (eCrestronSeries.Series3 == (series & eCrestronSeries.Series3)); + OnBoolChange(is3Series, 0, + JsonToSimplConstants.ProgramCompatibility3SeriesChange); + + var is4Series = (eCrestronSeries.Series4 == (series & eCrestronSeries.Series4)); + OnBoolChange(is4Series, 0, + JsonToSimplConstants.ProgramCompatibility4SeriesChange); + + var isServer = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server; + OnBoolChange(isServer, 0, + JsonToSimplConstants.DevicePlatformValueChange); + + // get the roomID + var roomId = Crestron.SimplSharp.InitialParametersClass.RoomId; + if (!string.IsNullOrEmpty(roomId)) { - OnBoolChange(false, 0, JsonToSimplConstants.JsonIsValidBoolChange); + OnStringChange(roomId, 0, JsonToSimplConstants.RoomIdChange); + } - var dirSeparator = Path.DirectorySeparatorChar; - var dirSeparatorAlt = Path.AltDirectorySeparatorChar; + // get the roomName + var roomName = Crestron.SimplSharp.InitialParametersClass.RoomName; + if (!string.IsNullOrEmpty(roomName)) + { + OnStringChange(roomName, 0, JsonToSimplConstants.RoomNameChange); + } - var series = CrestronEnvironment.ProgramCompatibility; + var rootDirectory = Directory.GetApplicationRootDirectory(); + OnStringChange(rootDirectory, 0, JsonToSimplConstants.RootDirectoryChange); + + var splusPath = string.Empty; + if (Regex.IsMatch(filepath, @"user", RegexOptions.IgnoreCase)) + { + if (is4Series) + splusPath = Regex.Replace(filepath, "user", "user", RegexOptions.IgnoreCase); + else if (isServer) + splusPath = Regex.Replace(filepath, "user", "User", RegexOptions.IgnoreCase); + else + splusPath = filepath; + } - var is3Series = (eCrestronSeries.Series3 == (series & eCrestronSeries.Series3)); - OnBoolChange(is3Series, 0, - JsonToSimplConstants.ProgramCompatibility3SeriesChange); + filepath = splusPath.Replace(dirSeparatorAlt, dirSeparator); + + Filepath = string.Format("{1}{0}{2}", dirSeparator, rootDirectory, + filepath.TrimStart(dirSeparator, dirSeparatorAlt)); + + OnStringChange(string.Format("Attempting to evaluate {0}", Filepath), 0, JsonToSimplConstants.StringValueChange); - var is4Series = (eCrestronSeries.Series4 == (series & eCrestronSeries.Series4)); - OnBoolChange(is4Series, 0, - JsonToSimplConstants.ProgramCompatibility4SeriesChange); + if (string.IsNullOrEmpty(Filepath)) + { + OnStringChange(string.Format("Cannot evaluate file. JSON file path not set"), 0, JsonToSimplConstants.StringValueChange); + CrestronConsole.PrintLine("Cannot evaluate file. JSON file path not set"); + return; + } - var isServer = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server; - OnBoolChange(isServer, 0, - JsonToSimplConstants.DevicePlatformValueChange); + // get file directory and name to search + var fileDirectory = Path.GetDirectoryName(Filepath); + var fileName = Path.GetFileName(Filepath); - // get the roomID - var roomId = Crestron.SimplSharp.InitialParametersClass.RoomId; - if (!string.IsNullOrEmpty(roomId)) + OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange); + Debug.Console(1, "Checking '{0}' for '{1}'", fileDirectory, fileName); + + if (Directory.Exists(fileDirectory)) + { + // get the directory info + var directoryInfo = new DirectoryInfo(fileDirectory); + + // get the file to be read + var actualFile = directoryInfo.GetFiles(fileName).FirstOrDefault(); + if (actualFile == null) { - OnStringChange(roomId, 0, JsonToSimplConstants.RoomIdChange); - } - - // get the roomName - var roomName = Crestron.SimplSharp.InitialParametersClass.RoomName; - if (!string.IsNullOrEmpty(roomName)) - { - OnStringChange(roomName, 0, JsonToSimplConstants.RoomNameChange); - } - - var rootDirectory = Directory.GetApplicationRootDirectory(); - OnStringChange(rootDirectory, 0, JsonToSimplConstants.RootDirectoryChange); - - var splusPath = string.Empty; - if (Regex.IsMatch(filepath, @"user", RegexOptions.IgnoreCase)) - { - if (is4Series) - splusPath = Regex.Replace(filepath, "user", "user", RegexOptions.IgnoreCase); - else if (isServer) - splusPath = Regex.Replace(filepath, "user", "User", RegexOptions.IgnoreCase); - else - splusPath = filepath; - } - - filepath = splusPath.Replace(dirSeparatorAlt, dirSeparator); - - Filepath = string.Format("{1}{0}{2}", dirSeparator, rootDirectory, - filepath.TrimStart(dirSeparator, dirSeparatorAlt)); - - OnStringChange(string.Format("Attempting to evaluate {0}", Filepath), 0, JsonToSimplConstants.StringValueChange); - - if (string.IsNullOrEmpty(Filepath)) - { - OnStringChange(string.Format("Cannot evaluate file. JSON file path not set"), 0, JsonToSimplConstants.StringValueChange); - CrestronConsole.PrintLine("Cannot evaluate file. JSON file path not set"); + var msg = string.Format("JSON file not found: {0}", Filepath); + OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange); + CrestronConsole.PrintLine(msg); + ErrorLog.Error(msg); return; } - // get file directory and name to search - var fileDirectory = Path.GetDirectoryName(Filepath); - var fileName = Path.GetFileName(Filepath); + // \xSE\xR\PDT000-Template_Main_Config-Combined_DSP_v00.02.json + // \USER\PDT000-Template_Main_Config-Combined_DSP_v00.02.json + ActualFilePath = actualFile.FullName; + OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange); + OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange); + Debug.Console(1, "Actual JSON file is {0}", ActualFilePath); - OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "Checking '{0}' for '{1}'", fileDirectory, fileName); - - if (Directory.Exists(fileDirectory)) - { - // get the directory info - var directoryInfo = new DirectoryInfo(fileDirectory); - - // get the file to be read - var actualFile = directoryInfo.GetFiles(fileName).FirstOrDefault(); - if (actualFile == null) - { - var msg = string.Format("JSON file not found: {0}", Filepath); - OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange); - CrestronConsole.PrintLine(msg); - ErrorLog.Error(msg); - return; - } - - // \xSE\xR\PDT000-Template_Main_Config-Combined_DSP_v00.02.json - // \USER\PDT000-Template_Main_Config-Combined_DSP_v00.02.json - ActualFilePath = actualFile.FullName; - OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange); - OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "Actual JSON file is {0}", ActualFilePath); - - Filename = actualFile.Name; - OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange); - OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "JSON Filename is {0}", Filename); + Filename = actualFile.Name; + OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange); + OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange); + Debug.Console(1, "JSON Filename is {0}", Filename); - FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator); - OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange); - OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "JSON File Path is {0}", FilePathName); + FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator); + OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange); + OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange); + Debug.Console(1, "JSON File Path is {0}", FilePathName); - var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII); + var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII); - JsonObject = JObject.Parse(json); - foreach (var child in Children) - child.ProcessAll(); + JsonObject = JObject.Parse(json); + foreach (var child in Children) + child.ProcessAll(); - OnBoolChange(true, 0, JsonToSimplConstants.JsonIsValidBoolChange); - } - else - { - OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "'{0}' not found", fileDirectory); - } + OnBoolChange(true, 0, JsonToSimplConstants.JsonIsValidBoolChange); } - catch (Exception e) + else { - var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message); - OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange); - CrestronConsole.PrintLine(msg); - ErrorLog.Error(msg); - - var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace); - OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange); - CrestronConsole.PrintLine(stackTrace); - ErrorLog.Error(stackTrace); + OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange); + Debug.Console(1, "'{0}' not found", fileDirectory); } } - - - /// - /// Sets the debug level - /// - /// - /// - /// setDebugLevel method - /// - public void setDebugLevel(uint level) + catch (Exception e) { - Debug.SetDebugLevel(level); + var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message); + OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange); + CrestronConsole.PrintLine(msg); + ErrorLog.Error(msg); + + var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace); + OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange); + CrestronConsole.PrintLine(stackTrace); + ErrorLog.Error(stackTrace); + } + } + + + /// + /// Sets the debug level + /// + /// + public void setDebugLevel(uint level) + { + Debug.SetDebugLevel(level); + } + + /// + /// Saves the values to the file + /// + public override void Save() + { + // this code is duplicated in the other masters!!!!!!!!!!!!! + UnsavedValues = new Dictionary(); + // Make each child update their values into master object + foreach (var child in Children) + { + Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key); + child.UpdateInputsForMaster(); } - /// - /// Saves the values to the file - /// - public override void Save() + if (UnsavedValues == null || UnsavedValues.Count == 0) { - // this code is duplicated in the other masters!!!!!!!!!!!!! - UnsavedValues = new Dictionary(); - // Make each child update their values into master object - foreach (var child in Children) + Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID); + return; + } + lock (FileLock) + { + Debug.Console(1, "Saving"); + foreach (var path in UnsavedValues.Keys) { - Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key); - child.UpdateInputsForMaster(); - } - - if (UnsavedValues == null || UnsavedValues.Count == 0) - { - Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID); - return; - } - lock (FileLock) - { - Debug.Console(1, "Saving"); - foreach (var path in UnsavedValues.Keys) - { - var tokenToReplace = JsonObject.SelectToken(path); - if (tokenToReplace != null) - {// It's found - tokenToReplace.Replace(UnsavedValues[path]); - Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path); - } - else // No token. Let's make one - { - //http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net - Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path); - - // JContainer jpart = JsonObject; - // // walk down the path and find where it goes - //#warning Does not handle arrays. - // foreach (var part in path.Split('.')) - // { - - // var openPos = part.IndexOf('['); - // if (openPos > -1) - // { - // openPos++; // move to number - // var closePos = part.IndexOf(']'); - // var arrayName = part.Substring(0, openPos - 1); // get the name - // var index = Convert.ToInt32(part.Substring(openPos, closePos - openPos)); - - // // Check if the array itself exists and add the item if so - // if (jpart[arrayName] != null) - // { - // var arrayObj = jpart[arrayName] as JArray; - // var item = arrayObj[index]; - // if (item == null) - // arrayObj.Add(new JObject()); - // } - - // Debug.Console(0, "IGNORING MISSING ARRAY VALUE FOR NOW"); - // continue; - // } - // // Build the - // if (jpart[part] == null) - // jpart.Add(new JProperty(part, new JObject())); - // jpart = jpart[part] as JContainer; - // } - // jpart.Replace(UnsavedValues[path]); - } + var tokenToReplace = JsonObject.SelectToken(path); + if (tokenToReplace != null) + {// It's found + tokenToReplace.Replace(UnsavedValues[path]); + Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path); } - using (StreamWriter sw = new StreamWriter(ActualFilePath)) + else // No token. Let's make one { - try - { - sw.Write(JsonObject.ToString()); - sw.Flush(); - } - catch (Exception e) - { - string err = string.Format("Error writing JSON file:\r{0}", e); - Debug.Console(0, err); - ErrorLog.Warn(err); - return; - } + //http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net + Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path); + + // JContainer jpart = JsonObject; + // // walk down the path and find where it goes + //#warning Does not handle arrays. + // foreach (var part in path.Split('.')) + // { + + // var openPos = part.IndexOf('['); + // if (openPos > -1) + // { + // openPos++; // move to number + // var closePos = part.IndexOf(']'); + // var arrayName = part.Substring(0, openPos - 1); // get the name + // var index = Convert.ToInt32(part.Substring(openPos, closePos - openPos)); + + // // Check if the array itself exists and add the item if so + // if (jpart[arrayName] != null) + // { + // var arrayObj = jpart[arrayName] as JArray; + // var item = arrayObj[index]; + // if (item == null) + // arrayObj.Add(new JObject()); + // } + + // Debug.Console(0, "IGNORING MISSING ARRAY VALUE FOR NOW"); + // continue; + // } + // // Build the + // if (jpart[part] == null) + // jpart.Add(new JProperty(part, new JObject())); + // jpart = jpart[part] as JContainer; + // } + // jpart.Replace(UnsavedValues[path]); + } + } + using (StreamWriter sw = new StreamWriter(ActualFilePath)) + { + try + { + sw.Write(JsonObject.ToString()); + sw.Flush(); + } + catch (Exception e) + { + string err = string.Format("Error writing JSON file:\r{0}", e); + Debug.Console(0, err); + ErrorLog.Warn(err); + return; } } } diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFixedPathObject.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFixedPathObject.cs index 9fa8ba01..6338e309 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFixedPathObject.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFixedPathObject.cs @@ -1,18 +1,17 @@  -namespace PepperDash.Core.JsonToSimpl -{ - /// - /// Represents a JsonToSimplFixedPathObject - /// +namespace PepperDash.Core.JsonToSimpl; + +/// +/// +/// public class JsonToSimplFixedPathObject : JsonToSimplChildObjectBase { - /// - /// Constructor - /// + /// + /// Constructor + /// public JsonToSimplFixedPathObject() { this.LinkedToObject = true; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs index c065f577..151773d9 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs @@ -6,35 +6,35 @@ using Crestron.SimplSharp; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; -namespace PepperDash.Core.JsonToSimpl -{ - /// - /// Represents a JsonToSimplGenericMaster - /// +namespace PepperDash.Core.JsonToSimpl; + +/// +/// Generic Master +/// public class JsonToSimplGenericMaster : JsonToSimplMaster - { +{ /*****************************************************************************************/ /** Privates **/ - + // The JSON file in JObject form // For gathering the incoming data object StringBuilderLock = new object(); // To prevent multiple same-file access static object WriteLock = new object(); - /// - /// Gets or sets the SaveCallback - /// + /// + /// Callback action for saving + /// public Action SaveCallback { get; set; } /*****************************************************************************************/ /// - /// SIMPL+ default constructor. - /// + /// SIMPL+ default constructor. + /// public JsonToSimplGenericMaster() - { + { } /// @@ -85,7 +85,7 @@ namespace PepperDash.Core.JsonToSimpl public override void Save() { // this code is duplicated in the other masters!!!!!!!!!!!!! - UnsavedValues = new Dictionary(); + UnsavedValues = new Dictionary(); // Make each child update their values into master object foreach (var child in Children) { @@ -121,5 +121,4 @@ namespace PepperDash.Core.JsonToSimpl else Debug.Console(0, this, "WARNING: No save callback defined."); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs index fcf6e0f8..2eb2a1d7 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs @@ -11,29 +11,29 @@ using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; using JsonSerializationException = NewtonsoftJson::Newtonsoft.Json.JsonSerializationException; using JsonTextReader = NewtonsoftJson::Newtonsoft.Json.JsonTextReader; -namespace PepperDash.Core.JsonToSimpl -{ - /// - /// Abstract base class for JsonToSimpl interactions - /// +namespace PepperDash.Core.JsonToSimpl; + +/// +/// Abstract base class for JsonToSimpl interactions +/// public abstract class JsonToSimplMaster : IKeyed { - /// - /// Notifies of bool change - /// + /// + /// Notifies of bool change + /// public event EventHandler BoolChange; - /// - /// Notifies of ushort change - /// + /// + /// Notifies of ushort change + /// public event EventHandler UshrtChange; - /// - /// Notifies of string change - /// + /// + /// Notifies of string change + /// public event EventHandler StringChange; - /// - /// A collection of associated child modules - /// + /// + /// A collection of associated child modules + /// protected List Children = new List(); /*****************************************************************************************/ @@ -43,9 +43,9 @@ namespace PepperDash.Core.JsonToSimpl /// public string Key { get { return UniqueID; } } - /// - /// Gets or sets the UniqueID - /// + /// + /// A unique ID + /// public string UniqueID { get; protected set; } /// @@ -87,9 +87,9 @@ namespace PepperDash.Core.JsonToSimpl } } - /// - /// Gets or sets the JsonObject - /// + /// + /// + /// public JObject JsonObject { get; protected set; } /*****************************************************************************************/ @@ -149,9 +149,9 @@ namespace PepperDash.Core.JsonToSimpl //Debug.Console(0, "Master[{0}] Unsaved size={1}", UniqueID, UnsavedValues.Count); } - /// - /// Saves the file - /// + /// + /// Saves the file + /// public abstract void Save(); @@ -160,14 +160,14 @@ namespace PepperDash.Core.JsonToSimpl /// public static class JsonFixes { - /// - /// Deserializes a string into a JObject - /// - /// - /// + /// + /// Deserializes a string into a JObject + /// + /// + /// public static JObject ParseObject(string json) { - using (var reader = new JsonTextReader(new System.IO.StringReader(json))) + using (var reader = new JsonTextReader(new System.IO.StringReader(json))) { var startDepth = reader.Depth; var obj = JObject.Load(reader); @@ -177,18 +177,15 @@ namespace PepperDash.Core.JsonToSimpl } } - /// - /// Deserializes a string into a JArray - /// - /// - /// - /// - /// ParseArray method - /// + /// + /// Deserializes a string into a JArray + /// + /// + /// public static JArray ParseArray(string json) { - using (var reader = new JsonTextReader(new System.IO.StringReader(json))) + using (var reader = new JsonTextReader(new System.IO.StringReader(json))) { var startDepth = reader.Depth; var obj = JArray.Load(reader); @@ -231,12 +228,12 @@ namespace PepperDash.Core.JsonToSimpl } } - /// - /// Helper event - /// - /// - /// - /// + /// + /// Helper event + /// + /// + /// + /// protected void OnStringChange(string value, ushort index, ushort type) { if (StringChange != null) @@ -247,4 +244,3 @@ namespace PepperDash.Core.JsonToSimpl } } } -} diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs index acadc99a..caba1c9b 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs @@ -9,22 +9,22 @@ using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; using PepperDash.Core.Config; -namespace PepperDash.Core.JsonToSimpl +namespace PepperDash.Core.JsonToSimpl; + +/// +/// Portal File Master +/// +public class JsonToSimplPortalFileMaster : JsonToSimplMaster { - /// - /// Portal File Master - /// - public class JsonToSimplPortalFileMaster : JsonToSimplMaster - { /// /// Sets the filepath as well as registers this with the Global.Masters list /// public string PortalFilepath { get; private set; } - /// - /// Gets or sets the ActualFilePath - /// - public string ActualFilePath { get; private set; } + /// + /// File path of the actual file being read (Portal or local) + /// + public string ActualFilePath { get; private set; } /*****************************************************************************************/ /** Privates **/ @@ -36,10 +36,10 @@ namespace PepperDash.Core.JsonToSimpl /*****************************************************************************************/ /// - /// SIMPL+ default constructor. - /// + /// SIMPL+ default constructor. + /// public JsonToSimplPortalFileMaster() - { + { } /// @@ -67,7 +67,7 @@ namespace PepperDash.Core.JsonToSimpl if (actualLocalFile != null) { ActualFilePath = actualLocalFile.FullName; - OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange); + OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange); } // If the local file does not exist, then read the portal file xyz.json // and create the local. @@ -81,7 +81,7 @@ namespace PepperDash.Core.JsonToSimpl // got the portal file, hand off to the merge / save method PortalConfigReader.ReadAndMergeFileIfNecessary(actualPortalFile.FullName, newLocalPath); ActualFilePath = newLocalPath; - OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange); + OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange); } else { @@ -194,4 +194,3 @@ namespace PepperDash.Core.JsonToSimpl } } } -} diff --git a/src/PepperDash.Core/Logging/CrestronEnricher.cs b/src/PepperDash.Core/Logging/CrestronEnricher.cs index c25306ca..1f879925 100644 --- a/src/PepperDash.Core/Logging/CrestronEnricher.cs +++ b/src/PepperDash.Core/Logging/CrestronEnricher.cs @@ -7,37 +7,30 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Core.Logging +namespace PepperDash.Core.Logging; + +public class CrestronEnricher : ILogEventEnricher { - /// - /// Represents a CrestronEnricher - /// - public class CrestronEnricher : ILogEventEnricher + static readonly string _appName; + + static CrestronEnricher() { - static readonly string _appName; - - static CrestronEnricher() + switch (CrestronEnvironment.DevicePlatform) { - switch (CrestronEnvironment.DevicePlatform) - { - case eDevicePlatform.Appliance: - _appName = $"App {InitialParametersClass.ApplicationNumber}"; - break; - case eDevicePlatform.Server: - _appName = $"{InitialParametersClass.RoomId}"; - break; - } - } - - - /// - /// Enrich method - /// - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - var property = propertyFactory.CreateProperty("App", _appName); - - logEvent.AddOrUpdateProperty(property); + case eDevicePlatform.Appliance: + _appName = $"App {InitialParametersClass.ApplicationNumber}"; + break; + case eDevicePlatform.Server: + _appName = $"{InitialParametersClass.RoomId}"; + break; } } + + + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + var property = propertyFactory.CreateProperty("App", _appName); + + logEvent.AddOrUpdateProperty(property); + } } diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 49d55eb4..6e4fb37d 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -20,8 +20,8 @@ using Serilog.Formatting.Compact; using Serilog.Formatting.Json; using Serilog.Templates; -namespace PepperDash.Core -{ +namespace PepperDash.Core; + /// /// public static class Debug @@ -1238,6 +1238,5 @@ namespace PepperDash.Core /// None /// None, - } } } \ No newline at end of file diff --git a/src/PepperDash.Core/Logging/DebugConsoleSink.cs b/src/PepperDash.Core/Logging/DebugConsoleSink.cs index 6363caa5..5e32a8a5 100644 --- a/src/PepperDash.Core/Logging/DebugConsoleSink.cs +++ b/src/PepperDash.Core/Logging/DebugConsoleSink.cs @@ -9,56 +9,45 @@ using System.IO; using System.Text; -namespace PepperDash.Core +namespace PepperDash.Core; + +public class DebugConsoleSink : ILogEventSink { - /// - /// Represents a DebugConsoleSink - /// - public class DebugConsoleSink : ILogEventSink + private readonly ITextFormatter _textFormatter; + + public void Emit(LogEvent logEvent) { - private readonly ITextFormatter _textFormatter; + if (!Debug.IsRunningOnAppliance) return; - /// - /// Emit method - /// - public void Emit(LogEvent logEvent) + /*string message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}]{logEvent.RenderMessage()}"; + + if(logEvent.Properties.TryGetValue("Key",out var value) && value is ScalarValue sv && sv.Value is string rawValue) { - if (!Debug.IsRunningOnAppliance) return; + message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}][{rawValue,3}]: {logEvent.RenderMessage()}"; + }*/ - /*string message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}]{logEvent.RenderMessage()}"; + var buffer = new StringWriter(new StringBuilder(256)); - if(logEvent.Properties.TryGetValue("Key",out var value) && value is ScalarValue sv && sv.Value is string rawValue) - { - message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}][{rawValue,3}]: {logEvent.RenderMessage()}"; - }*/ + _textFormatter.Format(logEvent, buffer); - var buffer = new StringWriter(new StringBuilder(256)); - - _textFormatter.Format(logEvent, buffer); - - var message = buffer.ToString(); - - CrestronConsole.PrintLine(message); - } - - public DebugConsoleSink(ITextFormatter formatProvider ) - { - _textFormatter = formatProvider ?? new JsonFormatter(); - } + var message = buffer.ToString(); + CrestronConsole.PrintLine(message); } - public static class DebugConsoleSinkExtensions + public DebugConsoleSink(ITextFormatter formatProvider ) { - /// - /// DebugConsoleSink method - /// - public static LoggerConfiguration DebugConsoleSink( - this LoggerSinkConfiguration loggerConfiguration, - ITextFormatter formatProvider = null) - { - return loggerConfiguration.Sink(new DebugConsoleSink(formatProvider)); - } + _textFormatter = formatProvider ?? new JsonFormatter(); } } + +public static class DebugConsoleSinkExtensions +{ + public static LoggerConfiguration DebugConsoleSink( + this LoggerSinkConfiguration loggerConfiguration, + ITextFormatter formatProvider = null) + { + return loggerConfiguration.Sink(new DebugConsoleSink(formatProvider)); + } +} diff --git a/src/PepperDash.Core/Logging/DebugContext.cs b/src/PepperDash.Core/Logging/DebugContext.cs index cdea6837..117140ab 100644 --- a/src/PepperDash.Core/Logging/DebugContext.cs +++ b/src/PepperDash.Core/Logging/DebugContext.cs @@ -9,288 +9,275 @@ using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Represents a debugging context +/// +public class DebugContext { /// - /// Represents a debugging context + /// Describes the folder location where a given program stores it's debug level memory. By default, the + /// file written will be named appNdebug where N is 1-10. /// - public class DebugContext + public string Key { get; private set; } + + ///// + ///// The name of the file containing the current debug settings. + ///// + //string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber); + + DebugContextSaveData SaveData; + + int SaveTimeoutMs = 30000; + + CTimer SaveTimer; + + + static List Contexts = new List(); + + /// + /// Creates or gets a debug context + /// + /// + /// + public static DebugContext GetDebugContext(string key) { - /// - /// Describes the folder location where a given program stores it's debug level memory. By default, the - /// file written will be named appNdebug where N is 1-10. - /// - public string Key { get; private set; } - - /// - /// The name of the file containing the current debug settings. - /// - //string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber); - - DebugContextSaveData SaveData; - - int SaveTimeoutMs = 30000; - - CTimer SaveTimer; - - - static List Contexts = new List(); - - /// - /// Creates or gets a debug context - /// - /// - /// - /// - /// GetDebugContext method - /// - public static DebugContext GetDebugContext(string key) + var context = Contexts.FirstOrDefault(c => c.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (context == null) { - var context = Contexts.FirstOrDefault(c => c.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (context == null) - { - context = new DebugContext(key); - Contexts.Add(context); - } - return context; + context = new DebugContext(key); + Contexts.Add(context); + } + return context; + } + + /// + /// Do not use. For S+ access. + /// + public DebugContext() { } + + DebugContext(string key) + { + Key = key; + if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) + { + // Add command to console + CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", + "appdebug:P [0-2]: Sets the application's console debug message level", + ConsoleAccessLevelEnum.AccessOperator); } - /// - /// Do not use. For S+ access. - /// - public DebugContext() { } + CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - DebugContext(string key) + LoadMemory(); + } + + /// + /// Used to save memory when shutting down + /// + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) { - Key = key; - if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) + if (SaveTimer != null) { - // Add command to console - CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", - "appdebug:P [0-2]: Sets the application's console debug message level", - ConsoleAccessLevelEnum.AccessOperator); + SaveTimer.Stop(); + SaveTimer = null; + } + Console(0, "Saving debug settings"); + SaveMemory(); + } + } + + /// + /// Callback for console command + /// + /// + public void SetDebugFromConsole(string levelString) + { + try + { + if (string.IsNullOrEmpty(levelString.Trim())) + { + CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", SaveData.Level); + return; } - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - - LoadMemory(); + SetDebugLevel(Convert.ToInt32(levelString)); } - - /// - /// Used to save memory when shutting down - /// - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + catch { - if (programEventType == eProgramStatusEventType.Stopping) - { - if (SaveTimer != null) - { - SaveTimer.Stop(); - SaveTimer = null; - } - Console(0, "Saving debug settings"); - SaveMemory(); - } + CrestronConsole.PrintLine("Usage: appdebug:P [0-2]"); } + } - /// - /// Callback for console command - /// - /// - /// - /// SetDebugFromConsole method - /// - public void SetDebugFromConsole(string levelString) + /// + /// Sets the debug level + /// + /// Valid values 0 (no debug), 1 (critical), 2 (all messages) + public void SetDebugLevel(int level) + { + if (level <= 2) { - try - { - if (string.IsNullOrEmpty(levelString.Trim())) - { - CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", SaveData.Level); - return; - } + SaveData.Level = level; + SaveMemoryOnTimeout(); - SetDebugLevel(Convert.ToInt32(levelString)); - } - catch - { - CrestronConsole.PrintLine("Usage: appdebug:P [0-2]"); - } + CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}", + InitialParametersClass.ApplicationNumber, SaveData.Level); } + } - /// - /// Sets the debug level - /// - /// Valid values 0 (no debug), 1 (critical), 2 (all messages) - /// - /// SetDebugLevel method - /// - public void SetDebugLevel(int level) + /// + /// Prints message to console if current debug level is equal to or higher than the level of this message. + /// Uses CrestronConsole.PrintLine. + /// + /// + /// Console format string + /// Object parameters + public void Console(uint level, string format, params object[] items) + { + if (SaveData.Level >= level) + CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, + string.Format(format, items)); + } + + /// + /// Appends a device Key to the beginning of a message + /// + public void Console(uint level, IKeyed dev, string format, params object[] items) + { + if (SaveData.Level >= level) + Console(level, "[{0}] {1}", dev.Key, string.Format(format, items)); + } + + /// + /// + /// + /// + /// + /// + /// + /// + public void Console(uint level, IKeyed dev, Debug.ErrorLogLevel errorLogLevel, + string format, params object[] items) + { + if (SaveData.Level >= level) { - if (level <= 2) - { - SaveData.Level = level; - SaveMemoryOnTimeout(); - - CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}", - InitialParametersClass.ApplicationNumber, SaveData.Level); - } - } - - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. - /// Uses CrestronConsole.PrintLine. - /// - /// - /// Console format string - /// Object parameters - public void Console(uint level, string format, params object[] items) - { - if (SaveData.Level >= level) - CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, - string.Format(format, items)); - } - - /// - /// Console method - /// - public void Console(uint level, IKeyed dev, string format, params object[] items) - { - if (SaveData.Level >= level) - Console(level, "[{0}] {1}", dev.Key, string.Format(format, items)); - } - - /// - /// - /// - /// - /// - /// - /// - /// - public void Console(uint level, IKeyed dev, Debug.ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - if (SaveData.Level >= level) - { - var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items)); - Console(level, str); - LogError(errorLogLevel, str); - } - } - - /// - /// - /// - /// - /// - /// - /// - public void Console(uint level, Debug.ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - if (SaveData.Level >= level) - { - var str = string.Format(format, items); - Console(level, str); - LogError(errorLogLevel, str); - } - } - - /// - /// - /// - /// - /// - /// - /// LogError method - /// - public void LogError(Debug.ErrorLogLevel errorLogLevel, string str) - { - string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); - switch (errorLogLevel) - { - case Debug.ErrorLogLevel.Error: - ErrorLog.Error(msg); - break; - case Debug.ErrorLogLevel.Warning: - ErrorLog.Warn(msg); - break; - case Debug.ErrorLogLevel.Notice: - ErrorLog.Notice(msg); - break; - } - } - - /// - /// Writes the memory object after timeout - /// - void SaveMemoryOnTimeout() - { - if (SaveTimer == null) - SaveTimer = new CTimer(o => - { - SaveTimer = null; - SaveMemory(); - }, SaveTimeoutMs); - else - SaveTimer.Reset(SaveTimeoutMs); - } - - /// - /// Writes the memory - use SaveMemoryOnTimeout - /// - void SaveMemory() - { - using (StreamWriter sw = new StreamWriter(GetMemoryFileName())) - { - var json = JsonConvert.SerializeObject(SaveData); - sw.Write(json); - sw.Flush(); - } - } - - /// - /// - /// - void LoadMemory() - { - var file = GetMemoryFileName(); - if (File.Exists(file)) - { - using (StreamReader sr = new StreamReader(file)) - { - var data = JsonConvert.DeserializeObject(sr.ReadToEnd()); - if (data != null) - { - SaveData = data; - Debug.Console(1, "Debug memory restored from file"); - return; - } - else - SaveData = new DebugContextSaveData(); - } - } - } - - /// - /// Helper to get the file path for this app's debug memory - /// - string GetMemoryFileName() - { - return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key); + var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items)); + Console(level, str); + LogError(errorLogLevel, str); } } /// /// /// - public class DebugContextSaveData + /// + /// + /// + /// + public void Console(uint level, Debug.ErrorLogLevel errorLogLevel, + string format, params object[] items) { - /// - /// - /// - public int Level { get; set; } + if (SaveData.Level >= level) + { + var str = string.Format(format, items); + Console(level, str); + LogError(errorLogLevel, str); + } } + + /// + /// + /// + /// + /// + public void LogError(Debug.ErrorLogLevel errorLogLevel, string str) + { + string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); + switch (errorLogLevel) + { + case Debug.ErrorLogLevel.Error: + ErrorLog.Error(msg); + break; + case Debug.ErrorLogLevel.Warning: + ErrorLog.Warn(msg); + break; + case Debug.ErrorLogLevel.Notice: + ErrorLog.Notice(msg); + break; + } + } + + /// + /// Writes the memory object after timeout + /// + void SaveMemoryOnTimeout() + { + if (SaveTimer == null) + SaveTimer = new CTimer(o => + { + SaveTimer = null; + SaveMemory(); + }, SaveTimeoutMs); + else + SaveTimer.Reset(SaveTimeoutMs); + } + + /// + /// Writes the memory - use SaveMemoryOnTimeout + /// + void SaveMemory() + { + using (StreamWriter sw = new StreamWriter(GetMemoryFileName())) + { + var json = JsonConvert.SerializeObject(SaveData); + sw.Write(json); + sw.Flush(); + } + } + + /// + /// + /// + void LoadMemory() + { + var file = GetMemoryFileName(); + if (File.Exists(file)) + { + using (StreamReader sr = new StreamReader(file)) + { + var data = JsonConvert.DeserializeObject(sr.ReadToEnd()); + if (data != null) + { + SaveData = data; + Debug.Console(1, "Debug memory restored from file"); + return; + } + else + SaveData = new DebugContextSaveData(); + } + } + } + + /// + /// Helper to get the file path for this app's debug memory + /// + string GetMemoryFileName() + { + return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key); + } +} + +/// +/// +/// +public class DebugContextSaveData +{ + /// + /// + /// + public int Level { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Core/Logging/DebugCrestronLoggerSink.cs b/src/PepperDash.Core/Logging/DebugCrestronLoggerSink.cs index 76859209..a2e82ec8 100644 --- a/src/PepperDash.Core/Logging/DebugCrestronLoggerSink.cs +++ b/src/PepperDash.Core/Logging/DebugCrestronLoggerSink.cs @@ -3,33 +3,26 @@ using Crestron.SimplSharp.CrestronLogger; using Serilog.Core; using Serilog.Events; -namespace PepperDash.Core.Logging +namespace PepperDash.Core.Logging; + +public class DebugCrestronLoggerSink : ILogEventSink { - /// - /// Represents a DebugCrestronLoggerSink - /// - public class DebugCrestronLoggerSink : ILogEventSink + public void Emit(LogEvent logEvent) { - /// - /// Emit method - /// - public void Emit(LogEvent logEvent) + if (!Debug.IsRunningOnAppliance) return; + + string message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}]{logEvent.RenderMessage()}"; + + if (logEvent.Properties.TryGetValue("Key", out var value) && value is ScalarValue sv && sv.Value is string rawValue) { - if (!Debug.IsRunningOnAppliance) return; - - string message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}]{logEvent.RenderMessage()}"; - - if (logEvent.Properties.TryGetValue("Key", out var value) && value is ScalarValue sv && sv.Value is string rawValue) - { - message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}][{rawValue}]: {logEvent.RenderMessage()}"; - } - - CrestronLogger.WriteToLog(message, (uint)logEvent.Level); + message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}][{rawValue}]: {logEvent.RenderMessage()}"; } - public DebugCrestronLoggerSink() - { - CrestronLogger.Initialize(1, LoggerModeEnum.RM); - } + CrestronLogger.WriteToLog(message, (uint)logEvent.Level); + } + + public DebugCrestronLoggerSink() + { + CrestronLogger.Initialize(1, LoggerModeEnum.RM); } } diff --git a/src/PepperDash.Core/Logging/DebugErrorLogSink.cs b/src/PepperDash.Core/Logging/DebugErrorLogSink.cs index 9ab4dc6b..3c2e6761 100644 --- a/src/PepperDash.Core/Logging/DebugErrorLogSink.cs +++ b/src/PepperDash.Core/Logging/DebugErrorLogSink.cs @@ -9,63 +9,56 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Core.Logging +namespace PepperDash.Core.Logging; + +public class DebugErrorLogSink : ILogEventSink { - /// - /// Represents a DebugErrorLogSink - /// - public class DebugErrorLogSink : ILogEventSink + private ITextFormatter _formatter; + + private Dictionary> _errorLogMap = new Dictionary> { - private ITextFormatter _formatter; + { LogEventLevel.Verbose, (msg) => ErrorLog.Notice(msg) }, + {LogEventLevel.Debug, (msg) => ErrorLog.Notice(msg) }, + {LogEventLevel.Information, (msg) => ErrorLog.Notice(msg) }, + {LogEventLevel.Warning, (msg) => ErrorLog.Warn(msg) }, + {LogEventLevel.Error, (msg) => ErrorLog.Error(msg) }, + {LogEventLevel.Fatal, (msg) => ErrorLog.Error(msg) } + }; + public void Emit(LogEvent logEvent) + { + string message; - private Dictionary> _errorLogMap = new Dictionary> + if (_formatter == null) { - { LogEventLevel.Verbose, (msg) => ErrorLog.Notice(msg) }, - {LogEventLevel.Debug, (msg) => ErrorLog.Notice(msg) }, - {LogEventLevel.Information, (msg) => ErrorLog.Notice(msg) }, - {LogEventLevel.Warning, (msg) => ErrorLog.Warn(msg) }, - {LogEventLevel.Error, (msg) => ErrorLog.Error(msg) }, - {LogEventLevel.Fatal, (msg) => ErrorLog.Error(msg) } - }; - /// - /// Emit method - /// - public void Emit(LogEvent logEvent) + var programId = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance + ? $"App {InitialParametersClass.ApplicationNumber}" + : $"Room {InitialParametersClass.RoomId}"; + + message = $"[{logEvent.Timestamp}][{logEvent.Level}][{programId}]{logEvent.RenderMessage()}"; + + if (logEvent.Properties.TryGetValue("Key", out var value) && value is ScalarValue sv && sv.Value is string rawValue) + { + message = $"[{logEvent.Timestamp}][{logEvent.Level}][{programId}][{rawValue}]: {logEvent.RenderMessage()}"; + } + } else { - string message; + var buffer = new StringWriter(new StringBuilder(256)); - if (_formatter == null) - { - var programId = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance - ? $"App {InitialParametersClass.ApplicationNumber}" - : $"Room {InitialParametersClass.RoomId}"; + _formatter.Format(logEvent, buffer); - message = $"[{logEvent.Timestamp}][{logEvent.Level}][{programId}]{logEvent.RenderMessage()}"; - - if (logEvent.Properties.TryGetValue("Key", out var value) && value is ScalarValue sv && sv.Value is string rawValue) - { - message = $"[{logEvent.Timestamp}][{logEvent.Level}][{programId}][{rawValue}]: {logEvent.RenderMessage()}"; - } - } else - { - var buffer = new StringWriter(new StringBuilder(256)); - - _formatter.Format(logEvent, buffer); - - message = buffer.ToString(); - } - - if(!_errorLogMap.TryGetValue(logEvent.Level, out var handler)) - { - return; - } - - handler(message); + message = buffer.ToString(); } - public DebugErrorLogSink(ITextFormatter formatter = null) + if(!_errorLogMap.TryGetValue(logEvent.Level, out var handler)) { - _formatter = formatter; + return; } + + handler(message); + } + + public DebugErrorLogSink(ITextFormatter formatter = null) + { + _formatter = formatter; } } diff --git a/src/PepperDash.Core/Logging/DebugExtensions.cs b/src/PepperDash.Core/Logging/DebugExtensions.cs index ccee4943..7ae40d97 100644 --- a/src/PepperDash.Core/Logging/DebugExtensions.cs +++ b/src/PepperDash.Core/Logging/DebugExtensions.cs @@ -2,112 +2,72 @@ using Serilog.Events; using Log = PepperDash.Core.Debug; -namespace PepperDash.Core.Logging +namespace PepperDash.Core.Logging; + +public static class DebugExtensions { - public static class DebugExtensions + public static void LogException(this IKeyed device, Exception ex, string message, params object[] args) { - /// - /// LogException method - /// - public static void LogException(this IKeyed device, Exception ex, string message, params object[] args) - { - Log.LogMessage(ex, message, device: device, args); - } + Log.LogMessage(ex, message, device, args); + } - /// - /// LogVerbose method - /// - public static void LogVerbose(this IKeyed device, Exception ex, string message, params object[] args) - { - Log.LogVerbose(ex, device, message, args); - } + public static void LogVerbose(this IKeyed device, Exception ex, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Verbose, ex, message, device, args); + } - /// - /// LogVerbose method - /// - public static void LogVerbose(this IKeyed device, string message, params object[] args) - { - Log.LogVerbose(device, message, args); - } + public static void LogVerbose(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Verbose, device, message, args); + } - /// - /// LogDebug method - /// - public static void LogDebug(this IKeyed device, Exception ex, string message, params object[] args) - { - Log.LogDebug(ex, device, message, args); - } + public static void LogDebug(this IKeyed device, Exception ex, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Debug, ex, message, device, args); + } - /// - /// LogDebug method - /// - public static void LogDebug(this IKeyed device, string message, params object[] args) - { - Log.LogDebug(device, message, args); - } + public static void LogDebug(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Debug, device, message, args); + } - /// - /// LogInformation method - /// - public static void LogInformation(this IKeyed device, Exception ex, string message, params object[] args) - { - Log.LogInformation(ex, device, message, args); - } + public static void LogInformation(this IKeyed device, Exception ex, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Information, ex, message, device, args); + } - /// - /// LogInformation method - /// - public static void LogInformation(this IKeyed device, string message, params object[] args) - { - Log.LogInformation(device, message, args); - } + public static void LogInformation(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Information, device, message, args); + } - /// - /// LogWarning method - /// - public static void LogWarning(this IKeyed device, Exception ex, string message, params object[] args) - { - Log.LogWarning(ex, device, message, args); - } + public static void LogWarning(this IKeyed device, Exception ex, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Warning, ex, message, device, args); + } - /// - /// LogWarning method - /// - public static void LogWarning(this IKeyed device, string message, params object[] args) - { - Log.LogWarning(device, message, args); - } + public static void LogWarning(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Warning, device, message, args); + } - /// - /// LogError method - /// - public static void LogError(this IKeyed device, Exception ex, string message, params object[] args) - { - Log.LogError(ex, device, message, args); - } + public static void LogError(this IKeyed device, Exception ex, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Error, ex, message, device, args); + } - /// - /// LogError method - /// - public static void LogError(this IKeyed device, string message, params object[] args) - { - Log.LogError(device, message, args); - } + public static void LogError(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Error, device, message, args); + } - /// - /// LogFatal method - /// - public static void LogFatal(this IKeyed device, Exception ex, string message, params object[] args) - { - Log.LogFatal(ex, device, message, args); - } + public static void LogFatal(this IKeyed device, Exception ex, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Fatal, ex, message, device, args); + } - /// - /// LogFatal method - /// - public static void LogFatal(this IKeyed device, string message, params object[] args) - { - Log.LogFatal(device, message, args); - } + public static void LogFatal(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Fatal, device, message, args); } } diff --git a/src/PepperDash.Core/Logging/DebugMemory.cs b/src/PepperDash.Core/Logging/DebugMemory.cs index 327d04b4..da327f77 100644 --- a/src/PepperDash.Core/Logging/DebugMemory.cs +++ b/src/PepperDash.Core/Logging/DebugMemory.cs @@ -4,25 +4,25 @@ using System.Collections.Generic; using Crestron.SimplSharp; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; -namespace PepperDash.Core.Logging -{ - /// - /// Represents a DebugContextCollection - /// +namespace PepperDash.Core.Logging; + +/// +/// Class to persist current Debug settings across program restarts +/// public class DebugContextCollection { - /// - /// To prevent threading issues with the DeviceDebugSettings collection - /// - private readonly CCriticalSection _deviceDebugSettingsLock; + /// + /// To prevent threading issues with the DeviceDebugSettings collection + /// + private readonly CCriticalSection _deviceDebugSettingsLock; [JsonProperty("items")] private readonly Dictionary _items; - /// - /// Collection of the debug settings for each device where the dictionary key is the device key - /// - [JsonProperty("deviceDebugSettings")] - private Dictionary DeviceDebugSettings { get; set; } + /// + /// Collection of the debug settings for each device where the dictionary key is the device key + /// + [JsonProperty("deviceDebugSettings")] + private Dictionary DeviceDebugSettings { get; set; } /// @@ -30,8 +30,8 @@ namespace PepperDash.Core.Logging /// public DebugContextCollection() { - _deviceDebugSettingsLock = new CCriticalSection(); - DeviceDebugSettings = new Dictionary(); + _deviceDebugSettingsLock = new CCriticalSection(); + DeviceDebugSettings = new Dictionary(); _items = new Dictionary(); } @@ -67,46 +67,40 @@ namespace PepperDash.Core.Logging } - /// - /// sets the settings for a device or creates a new entry - /// - /// - /// - /// - /// - /// SetDebugSettingsForKey method - /// - public void SetDebugSettingsForKey(string deviceKey, object settings) + /// + /// sets the settings for a device or creates a new entry + /// + /// + /// + /// + public void SetDebugSettingsForKey(string deviceKey, object settings) + { + try { - try - { - _deviceDebugSettingsLock.Enter(); + _deviceDebugSettingsLock.Enter(); - if (DeviceDebugSettings.ContainsKey(deviceKey)) - { - DeviceDebugSettings[deviceKey] = settings; - } - else - DeviceDebugSettings.Add(deviceKey, settings); - } - finally + if (DeviceDebugSettings.ContainsKey(deviceKey)) { - _deviceDebugSettingsLock.Leave(); + DeviceDebugSettings[deviceKey] = settings; } + else + DeviceDebugSettings.Add(deviceKey, settings); } - - /// - /// Gets the device settings for a device by key or returns null - /// - /// - /// - /// - /// GetDebugSettingsForKey method - /// - public object GetDebugSettingsForKey(string deviceKey) + finally { - return DeviceDebugSettings[deviceKey]; + _deviceDebugSettingsLock.Leave(); } + } + + /// + /// Gets the device settings for a device by key or returns null + /// + /// + /// + public object GetDebugSettingsForKey(string deviceKey) + { + return DeviceDebugSettings[deviceKey]; + } } /// @@ -114,16 +108,15 @@ namespace PepperDash.Core.Logging /// public class DebugContextItem { - /// - /// The level of debug messages to print - /// + /// + /// The level of debug messages to print + /// [JsonProperty("level")] public int Level { get; set; } - /// - /// Property to tell the program not to intitialize when it boots, if desired - /// - [JsonProperty("doNotLoadOnNextBoot")] - public bool DoNotLoadOnNextBoot { get; set; } - } -} \ No newline at end of file + /// + /// Property to tell the program not to intitialize when it boots, if desired + /// + [JsonProperty("doNotLoadOnNextBoot")] + public bool DoNotLoadOnNextBoot { get; set; } + } \ No newline at end of file diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 81d981ec..d62b3c8b 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -1,10 +1,6 @@ extern alias NewtonsoftJson; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Crestron.SimplSharp; using Org.BouncyCastle.Asn1.X509; using Serilog; diff --git a/src/PepperDash.Core/Network/DiscoveryThings.cs b/src/PepperDash.Core/Network/DiscoveryThings.cs index 973c03a4..c01613b7 100644 --- a/src/PepperDash.Core/Network/DiscoveryThings.cs +++ b/src/PepperDash.Core/Network/DiscoveryThings.cs @@ -4,19 +4,17 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core -{ +namespace PepperDash.Core; + +/// +/// Not in use +/// + public static class NetworkComm + { /// /// Not in use /// - public static class NetworkComm - { - /// - /// Not in use - /// static NetworkComm() { } - } - -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/PasswordManagement/Config.cs b/src/PepperDash.Core/PasswordManagement/Config.cs index 22aa4881..a5f071a4 100644 --- a/src/PepperDash.Core/PasswordManagement/Config.cs +++ b/src/PepperDash.Core/PasswordManagement/Config.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.PasswordManagement -{ +namespace PepperDash.Core.PasswordManagement; + /// /// JSON password configuration /// @@ -22,5 +22,4 @@ namespace PepperDash.Core.PasswordManagement { } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/PasswordManagement/Constants.cs b/src/PepperDash.Core/PasswordManagement/Constants.cs index 65a1bf45..d4cf1e0b 100644 --- a/src/PepperDash.Core/PasswordManagement/Constants.cs +++ b/src/PepperDash.Core/PasswordManagement/Constants.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.PasswordManagement -{ +namespace PepperDash.Core.PasswordManagement; + /// /// Constants /// @@ -53,5 +53,4 @@ namespace PepperDash.Core.PasswordManagement /// Generic string value change constant /// public const ushort StringValueChange = 201; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/PasswordManagement/PasswordClient.cs b/src/PepperDash.Core/PasswordManagement/PasswordClient.cs index 9443be0b..4a6c7368 100644 --- a/src/PepperDash.Core/PasswordManagement/PasswordClient.cs +++ b/src/PepperDash.Core/PasswordManagement/PasswordClient.cs @@ -1,10 +1,10 @@ using System; -namespace PepperDash.Core.PasswordManagement -{ - /// - /// Represents a PasswordClient - /// +namespace PepperDash.Core.PasswordManagement; + +/// +/// A class to allow user interaction with the PasswordManager +/// public class PasswordClient { /// @@ -192,5 +192,4 @@ namespace PepperDash.Core.PasswordManagement GetPasswordByIndex(args.Index); } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/PasswordManagement/PasswordManager.cs b/src/PepperDash.Core/PasswordManagement/PasswordManager.cs index 9ba008e4..d08a1010 100644 --- a/src/PepperDash.Core/PasswordManagement/PasswordManager.cs +++ b/src/PepperDash.Core/PasswordManagement/PasswordManager.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using Crestron.SimplSharp; -namespace PepperDash.Core.PasswordManagement -{ - /// - /// Represents a PasswordManager - /// +namespace PepperDash.Core.PasswordManagement; + +/// +/// Allows passwords to be stored and managed +/// public class PasswordManager { /// @@ -196,7 +196,7 @@ namespace PepperDash.Core.PasswordManagement /// /// Protected ushort change event handler /// - /// + /// /// /// protected void OnUshrtChange(ushort value, ushort index, ushort type) @@ -243,5 +243,4 @@ namespace PepperDash.Core.PasswordManagement PasswordChange(this, args); } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/SystemInfo/EventArgs and Constants.cs b/src/PepperDash.Core/SystemInfo/EventArgs and Constants.cs index a83f8901..e6d697f3 100644 --- a/src/PepperDash.Core/SystemInfo/EventArgs and Constants.cs +++ b/src/PepperDash.Core/SystemInfo/EventArgs and Constants.cs @@ -4,68 +4,68 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.SystemInfo -{ +namespace PepperDash.Core.SystemInfo; + /// /// Constants /// public class SystemInfoConstants { - /// - /// - /// + /// + /// + /// public const ushort BoolValueChange = 1; /// /// /// - public const ushort CompleteBoolChange = 2; + public const ushort CompleteBoolChange = 2; /// /// /// - public const ushort BusyBoolChange = 3; - - /// - /// - /// + public const ushort BusyBoolChange = 3; + + /// + /// + /// public const ushort UshortValueChange = 101; - /// - /// - /// + /// + /// + /// public const ushort StringValueChange = 201; /// /// /// - public const ushort ConsoleResponseChange = 202; + public const ushort ConsoleResponseChange = 202; /// /// /// - public const ushort ProcessorUptimeChange = 203; + public const ushort ProcessorUptimeChange = 203; /// /// /// - public const ushort ProgramUptimeChange = 204; + public const ushort ProgramUptimeChange = 204; - /// - /// - /// + /// + /// + /// public const ushort ObjectChange = 301; /// /// /// - public const ushort ProcessorConfigChange = 302; + public const ushort ProcessorConfigChange = 302; /// /// /// - public const ushort EthernetConfigChange = 303; + public const ushort EthernetConfigChange = 303; /// /// /// - public const ushort ControlSubnetConfigChange = 304; + public const ushort ControlSubnetConfigChange = 304; /// /// /// - public const ushort ProgramConfigChange = 305; + public const ushort ProgramConfigChange = 305; } /// @@ -73,18 +73,18 @@ namespace PepperDash.Core.SystemInfo /// public class ProcessorChangeEventArgs : EventArgs { - /// - /// - /// + /// + /// + /// public ProcessorInfo Processor { get; set; } /// /// /// - public ushort Type { get; set; } + public ushort Type { get; set; } /// /// /// - public ushort Index { get; set; } + public ushort Index { get; set; } /// /// Constructor @@ -119,18 +119,18 @@ namespace PepperDash.Core.SystemInfo /// public class EthernetChangeEventArgs : EventArgs { - /// - /// - /// + /// + /// + /// public EthernetInfo Adapter { get; set; } /// /// /// - public ushort Type { get; set; } + public ushort Type { get; set; } /// /// /// - public ushort Index { get; set; } + public ushort Index { get; set; } /// /// Constructor @@ -143,7 +143,7 @@ namespace PepperDash.Core.SystemInfo /// /// Constructor overload /// - /// + /// /// public EthernetChangeEventArgs(EthernetInfo ethernet, ushort type) { @@ -154,9 +154,9 @@ namespace PepperDash.Core.SystemInfo /// /// Constructor overload /// - /// + /// /// - /// + /// public EthernetChangeEventArgs(EthernetInfo ethernet, ushort type, ushort index) { Adapter = ethernet; @@ -170,18 +170,18 @@ namespace PepperDash.Core.SystemInfo /// public class ControlSubnetChangeEventArgs : EventArgs { - /// - /// - /// + /// + /// + /// public ControlSubnetInfo Adapter { get; set; } /// /// /// - public ushort Type { get; set; } + public ushort Type { get; set; } /// /// /// - public ushort Index { get; set; } + public ushort Index { get; set; } /// /// Constructor @@ -216,18 +216,18 @@ namespace PepperDash.Core.SystemInfo /// public class ProgramChangeEventArgs : EventArgs { - /// - /// - /// + /// + /// + /// public ProgramInfo Program { get; set; } /// /// /// - public ushort Type { get; set; } + public ushort Type { get; set; } /// /// /// - public ushort Index { get; set; } + public ushort Index { get; set; } /// /// Constructor @@ -240,7 +240,7 @@ namespace PepperDash.Core.SystemInfo /// /// Constructor overload /// - /// + /// /// public ProgramChangeEventArgs(ProgramInfo program, ushort type) { @@ -251,14 +251,13 @@ namespace PepperDash.Core.SystemInfo /// /// Constructor overload /// - /// + /// /// - /// + /// public ProgramChangeEventArgs(ProgramInfo program, ushort type, ushort index) { Program = program; Type = type; Index = index; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/SystemInfo/SystemInfoConfig.cs b/src/PepperDash.Core/SystemInfo/SystemInfoConfig.cs index 8dc3acaf..950abecb 100644 --- a/src/PepperDash.Core/SystemInfo/SystemInfoConfig.cs +++ b/src/PepperDash.Core/SystemInfo/SystemInfoConfig.cs @@ -4,52 +4,52 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.SystemInfo -{ +namespace PepperDash.Core.SystemInfo; + /// /// Processor info class /// public class ProcessorInfo { - /// - /// - /// + /// + /// + /// public string Model { get; set; } - /// - /// - /// + /// + /// + /// public string SerialNumber { get; set; } - /// - /// - /// + /// + /// + /// public string Firmware { get; set; } - /// - /// - /// + /// + /// + /// public string FirmwareDate { get; set; } - /// - /// - /// + /// + /// + /// public string OsVersion { get; set; } - /// - /// - /// + /// + /// + /// public string RuntimeEnvironment { get; set; } - /// - /// - /// + /// + /// + /// public string DevicePlatform { get; set; } - /// - /// - /// + /// + /// + /// public string ModuleDirectory { get; set; } - /// - /// - /// + /// + /// + /// public string LocalTimeZone { get; set; } - /// - /// - /// + /// + /// + /// public string ProgramIdTag { get; set; } /// @@ -66,45 +66,45 @@ namespace PepperDash.Core.SystemInfo /// public class EthernetInfo { - /// - /// - /// + /// + /// + /// public ushort DhcpIsOn { get; set; } - /// - /// - /// + /// + /// + /// public string Hostname { get; set; } - /// - /// - /// + /// + /// + /// public string MacAddress { get; set; } - /// - /// - /// + /// + /// + /// public string IpAddress { get; set; } - /// - /// - /// + /// + /// + /// public string Subnet { get; set; } - /// - /// - /// + /// + /// + /// public string Gateway { get; set; } - /// - /// - /// + /// + /// + /// public string Dns1 { get; set; } - /// - /// - /// + /// + /// + /// public string Dns2 { get; set; } - /// - /// - /// + /// + /// + /// public string Dns3 { get; set; } - /// - /// - /// + /// + /// + /// public string Domain { get; set; } /// @@ -121,29 +121,29 @@ namespace PepperDash.Core.SystemInfo /// public class ControlSubnetInfo { - /// - /// - /// + /// + /// + /// public ushort Enabled { get; set; } - /// - /// - /// + /// + /// + /// public ushort IsInAutomaticMode { get; set; } - /// - /// - /// + /// + /// + /// public string MacAddress { get; set; } - /// - /// - /// + /// + /// + /// public string IpAddress { get; set; } - /// - /// - /// + /// + /// + /// public string Subnet { get; set; } - /// - /// - /// + /// + /// + /// public string RouterPrefix { get; set; } /// @@ -160,37 +160,37 @@ namespace PepperDash.Core.SystemInfo /// public class ProgramInfo { - /// - /// - /// + /// + /// + /// public string Name { get; set; } - /// - /// - /// + /// + /// + /// public string Header { get; set; } - /// - /// - /// + /// + /// + /// public string System { get; set; } - /// - /// - /// + /// + /// + /// public string ProgramIdTag { get; set; } - /// - /// - /// + /// + /// + /// public string CompileTime { get; set; } - /// - /// - /// + /// + /// + /// public string Database { get; set; } - /// - /// - /// + /// + /// + /// public string Environment { get; set; } - /// - /// - /// + /// + /// + /// public string Programmer { get; set; } /// @@ -200,5 +200,4 @@ namespace PepperDash.Core.SystemInfo { } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/SystemInfo/SystemInfoToSimpl.cs b/src/PepperDash.Core/SystemInfo/SystemInfoToSimpl.cs index 45f787f5..00309676 100644 --- a/src/PepperDash.Core/SystemInfo/SystemInfoToSimpl.cs +++ b/src/PepperDash.Core/SystemInfo/SystemInfoToSimpl.cs @@ -4,37 +4,37 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.SystemInfo -{ +namespace PepperDash.Core.SystemInfo; + /// /// System Info class /// public class SystemInfoToSimpl { - /// - /// Notifies of bool change - /// + /// + /// Notifies of bool change + /// public event EventHandler BoolChange; - /// - /// Notifies of string change - /// + /// + /// Notifies of string change + /// public event EventHandler StringChange; - /// - /// Notifies of processor change - /// + /// + /// Notifies of processor change + /// public event EventHandler ProcessorChange; - /// - /// Notifies of ethernet change - /// + /// + /// Notifies of ethernet change + /// public event EventHandler EthernetChange; - /// - /// Notifies of control subnet change - /// + /// + /// Notifies of control subnet change + /// public event EventHandler ControlSubnetChange; - /// - /// Notifies of program change - /// + /// + /// Notifies of program change + /// public event EventHandler ProgramChange; /// @@ -336,10 +336,10 @@ namespace PepperDash.Core.SystemInfo /// /// private method to parse console messages /// - /// + /// /// - /// - /// + /// + /// /// private string ParseConsoleResponse(string data, string line, string dataStart, string dataEnd) { @@ -467,5 +467,4 @@ namespace PepperDash.Core.SystemInfo ProgramChange(this, args); } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/Web/BouncyCertificate.cs b/src/PepperDash.Core/Web/BouncyCertificate.cs index 9e885094..148b6eb1 100644 --- a/src/PepperDash.Core/Web/BouncyCertificate.cs +++ b/src/PepperDash.Core/Web/BouncyCertificate.cs @@ -20,352 +20,336 @@ using Org.BouncyCastle.Crypto.Operators; using BigInteger = Org.BouncyCastle.Math.BigInteger; using X509Certificate = Org.BouncyCastle.X509.X509Certificate; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/ +/// +internal class BouncyCertificate { - /// - /// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/ - /// - internal class BouncyCertificate + public string CertificatePassword { get; set; } = "password"; + public X509Certificate2 LoadCertificate(string issuerFileName, string password) { - public string CertificatePassword { get; set; } = "password"; - public X509Certificate2 LoadCertificate(string issuerFileName, string password) + // We need to pass 'Exportable', otherwise we can't get the private key. + var issuerCertificate = new X509Certificate2(issuerFileName, password, X509KeyStorageFlags.Exportable); + return issuerCertificate; + } + + public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages) + { + // It's self-signed, so these are the same. + var issuerName = issuerCertificate.Subject; + + var random = GetSecureRandom(); + var subjectKeyPair = GenerateKeyPair(random, 2048); + + var issuerKeyPair = DotNetUtilities.GetKeyPair(issuerCertificate.PrivateKey); + + var serialNumber = GenerateSerialNumber(random); + var issuerSerialNumber = new BigInteger(issuerCertificate.GetSerialNumber()); + + const bool isCertificateAuthority = false; + var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, + subjectAlternativeNames, issuerName, issuerKeyPair, + issuerSerialNumber, isCertificateAuthority, + usages); + return ConvertCertificate(certificate, subjectKeyPair, random); + } + + public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages) + { + // It's self-signed, so these are the same. + var issuerName = subjectName; + + var random = GetSecureRandom(); + var subjectKeyPair = GenerateKeyPair(random, 2048); + + // It's self-signed, so these are the same. + var issuerKeyPair = subjectKeyPair; + + var serialNumber = GenerateSerialNumber(random); + var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number. + + const bool isCertificateAuthority = true; + var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, + subjectAlternativeNames, issuerName, issuerKeyPair, + issuerSerialNumber, isCertificateAuthority, + usages); + return ConvertCertificate(certificate, subjectKeyPair, random); + } + + public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages) + { + // It's self-signed, so these are the same. + var issuerName = subjectName; + + var random = GetSecureRandom(); + var subjectKeyPair = GenerateKeyPair(random, 2048); + + // It's self-signed, so these are the same. + var issuerKeyPair = subjectKeyPair; + + var serialNumber = GenerateSerialNumber(random); + var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number. + + const bool isCertificateAuthority = false; + var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, + subjectAlternativeNames, issuerName, issuerKeyPair, + issuerSerialNumber, isCertificateAuthority, + usages); + return ConvertCertificate(certificate, subjectKeyPair, random); + } + + private SecureRandom GetSecureRandom() + { + // Since we're on Windows, we'll use the CryptoAPI one (on the assumption + // that it might have access to better sources of entropy than the built-in + // Bouncy Castle ones): + var randomGenerator = new CryptoApiRandomGenerator(); + var random = new SecureRandom(randomGenerator); + return random; + } + + private X509Certificate GenerateCertificate(SecureRandom random, + string subjectName, + AsymmetricCipherKeyPair subjectKeyPair, + BigInteger subjectSerialNumber, + string[] subjectAlternativeNames, + string issuerName, + AsymmetricCipherKeyPair issuerKeyPair, + BigInteger issuerSerialNumber, + bool isCertificateAuthority, + KeyPurposeID[] usages) + { + var certificateGenerator = new X509V3CertificateGenerator(); + + certificateGenerator.SetSerialNumber(subjectSerialNumber); + + var issuerDN = new X509Name(issuerName); + certificateGenerator.SetIssuerDN(issuerDN); + + // Note: The subject can be omitted if you specify a subject alternative name (SAN). + var subjectDN = new X509Name(subjectName); + certificateGenerator.SetSubjectDN(subjectDN); + + // Our certificate needs valid from/to values. + var notBefore = DateTime.UtcNow.Date; + var notAfter = notBefore.AddYears(2); + + certificateGenerator.SetNotBefore(notBefore); + certificateGenerator.SetNotAfter(notAfter); + + // The subject's public key goes in the certificate. + certificateGenerator.SetPublicKey(subjectKeyPair.Public); + + AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber); + AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair); + //AddBasicConstraints(certificateGenerator, isCertificateAuthority); + + if (usages != null && usages.Any()) + AddExtendedKeyUsage(certificateGenerator, usages); + + if (subjectAlternativeNames != null && subjectAlternativeNames.Any()) + AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames); + + // Set the signature algorithm. This is used to generate the thumbprint which is then signed + // with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong. + const string signatureAlgorithm = "SHA256WithRSA"; + + // The certificate is signed with the issuer's private key. + ISignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private, random); + var certificate = certificateGenerator.Generate(signatureFactory); + return certificate; + } + + /// + /// The certificate needs a serial number. This is used for revocation, + /// and usually should be an incrementing index (which makes it easier to revoke a range of certificates). + /// Since we don't have anywhere to store the incrementing index, we can just use a random number. + /// + /// + /// + private BigInteger GenerateSerialNumber(SecureRandom random) + { + var serialNumber = + BigIntegers.CreateRandomInRange( + BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random); + return serialNumber; + } + + /// + /// Generate a key pair. + /// + /// The random number generator. + /// The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days. + /// + private AsymmetricCipherKeyPair GenerateKeyPair(SecureRandom random, int strength) + { + var keyGenerationParameters = new KeyGenerationParameters(random, strength); + + var keyPairGenerator = new RsaKeyPairGenerator(); + keyPairGenerator.Init(keyGenerationParameters); + var subjectKeyPair = keyPairGenerator.GenerateKeyPair(); + return subjectKeyPair; + } + + /// + /// Add the Authority Key Identifier. According to http://www.alvestrand.no/objectid/2.5.29.35.html, this + /// identifies the public key to be used to verify the signature on this certificate. + /// In a certificate chain, this corresponds to the "Subject Key Identifier" on the *issuer* certificate. + /// The Bouncy Castle documentation, at http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation, + /// shows how to create this from the issuing certificate. Since we're creating a self-signed certificate, we have to do this slightly differently. + /// + /// + /// + /// + /// + private void AddAuthorityKeyIdentifier(X509V3CertificateGenerator certificateGenerator, + X509Name issuerDN, + AsymmetricCipherKeyPair issuerKeyPair, + BigInteger issuerSerialNumber) + { + var authorityKeyIdentifierExtension = + new AuthorityKeyIdentifier( + SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public), + new GeneralNames(new GeneralName(issuerDN)), + issuerSerialNumber); + certificateGenerator.AddExtension( + X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension); + } + + /// + /// Add the "Subject Alternative Names" extension. Note that you have to repeat + /// the value from the "Subject Name" property. + /// + /// + /// + private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator, + IEnumerable subjectAlternativeNames) + { + var subjectAlternativeNamesExtension = + new DerSequence( + subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name)) + .ToArray()); + certificateGenerator.AddExtension( + X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension); + } + + /// + /// Add the "Extended Key Usage" extension, specifying (for example) "server authentication". + /// + /// + /// + private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages) + { + certificateGenerator.AddExtension( + X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages)); + } + + /// + /// Add the "Basic Constraints" extension. + /// + /// + /// + private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator, + bool isCertificateAuthority) + { + certificateGenerator.AddExtension( + X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority)); + } + + /// + /// Add the Subject Key Identifier. + /// + /// + /// + private void AddSubjectKeyIdentifier(X509V3CertificateGenerator certificateGenerator, + AsymmetricCipherKeyPair subjectKeyPair) + { + var subjectKeyIdentifierExtension = + new SubjectKeyIdentifier( + SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(subjectKeyPair.Public)); + certificateGenerator.AddExtension( + X509Extensions.SubjectKeyIdentifier.Id, false, subjectKeyIdentifierExtension); + } + + private X509Certificate2 ConvertCertificate(X509Certificate certificate, + AsymmetricCipherKeyPair subjectKeyPair, + SecureRandom random) + { + // Now to convert the Bouncy Castle certificate to a .NET certificate. + // See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx + // ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and private key to that. + var store = new Pkcs12StoreBuilder().Build(); + + // What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name". + string friendlyName = certificate.SubjectDN.ToString(); + + // Add the certificate. + var certificateEntry = new X509CertificateEntry(certificate); + store.SetCertificateEntry(friendlyName, certificateEntry); + + // Add the private key. + store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry }); + + // Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream. + // It needs a password. Since we'll remove this later, it doesn't particularly matter what we use. + + var stream = new MemoryStream(); + store.Save(stream, CertificatePassword.ToCharArray(), random); + + var convertedCertificate = + new X509Certificate2(stream.ToArray(), + CertificatePassword, + X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); + return convertedCertificate; + } + + public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName) + { + // This password is the one attached to the PFX file. Use 'null' for no password. + // Create PFX (PKCS #12) with private key + try { - // We need to pass 'Exportable', otherwise we can't get the private key. - var issuerCertificate = new X509Certificate2(issuerFileName, password, X509KeyStorageFlags.Exportable); - return issuerCertificate; + var pfx = certificate.Export(X509ContentType.Pfx, CertificatePassword); + File.WriteAllBytes(string.Format("{0}.pfx", Path.Combine(outputDirectory, certName)), pfx); } - - /// - /// IssueCertificate method - /// - public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages) + catch (Exception ex) { - // It's self-signed, so these are the same. - var issuerName = issuerCertificate.Subject; - - var random = GetSecureRandom(); - var subjectKeyPair = GenerateKeyPair(random, 2048); - - var issuerKeyPair = DotNetUtilities.GetKeyPair(issuerCertificate.PrivateKey); - - var serialNumber = GenerateSerialNumber(random); - var issuerSerialNumber = new BigInteger(issuerCertificate.GetSerialNumber()); - - const bool isCertificateAuthority = false; - var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, - subjectAlternativeNames, issuerName, issuerKeyPair, - issuerSerialNumber, isCertificateAuthority, - usages); - return ConvertCertificate(certificate, subjectKeyPair, random); + CrestronConsole.PrintLine(string.Format("Failed to write x509 cert pfx\r\n{0}", ex.Message)); } - - /// - /// CreateCertificateAuthorityCertificate method - /// - public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages) + // Create Base 64 encoded CER (public key only) + using (var writer = new StreamWriter($"{Path.Combine(outputDirectory, certName)}.cer", false)) { - // It's self-signed, so these are the same. - var issuerName = subjectName; - - var random = GetSecureRandom(); - var subjectKeyPair = GenerateKeyPair(random, 2048); - - // It's self-signed, so these are the same. - var issuerKeyPair = subjectKeyPair; - - var serialNumber = GenerateSerialNumber(random); - var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number. - - const bool isCertificateAuthority = true; - var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, - subjectAlternativeNames, issuerName, issuerKeyPair, - issuerSerialNumber, isCertificateAuthority, - usages); - return ConvertCertificate(certificate, subjectKeyPair, random); - } - - /// - /// CreateSelfSignedCertificate method - /// - public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages) - { - // It's self-signed, so these are the same. - var issuerName = subjectName; - - var random = GetSecureRandom(); - var subjectKeyPair = GenerateKeyPair(random, 2048); - - // It's self-signed, so these are the same. - var issuerKeyPair = subjectKeyPair; - - var serialNumber = GenerateSerialNumber(random); - var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number. - - const bool isCertificateAuthority = false; - var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber, - subjectAlternativeNames, issuerName, issuerKeyPair, - issuerSerialNumber, isCertificateAuthority, - usages); - return ConvertCertificate(certificate, subjectKeyPair, random); - } - - private SecureRandom GetSecureRandom() - { - // Since we're on Windows, we'll use the CryptoAPI one (on the assumption - // that it might have access to better sources of entropy than the built-in - // Bouncy Castle ones): - var randomGenerator = new CryptoApiRandomGenerator(); - var random = new SecureRandom(randomGenerator); - return random; - } - - private X509Certificate GenerateCertificate(SecureRandom random, - string subjectName, - AsymmetricCipherKeyPair subjectKeyPair, - BigInteger subjectSerialNumber, - string[] subjectAlternativeNames, - string issuerName, - AsymmetricCipherKeyPair issuerKeyPair, - BigInteger issuerSerialNumber, - bool isCertificateAuthority, - KeyPurposeID[] usages) - { - var certificateGenerator = new X509V3CertificateGenerator(); - - certificateGenerator.SetSerialNumber(subjectSerialNumber); - - var issuerDN = new X509Name(issuerName); - certificateGenerator.SetIssuerDN(issuerDN); - - // Note: The subject can be omitted if you specify a subject alternative name (SAN). - var subjectDN = new X509Name(subjectName); - certificateGenerator.SetSubjectDN(subjectDN); - - // Our certificate needs valid from/to values. - var notBefore = DateTime.UtcNow.Date; - var notAfter = notBefore.AddYears(2); - - certificateGenerator.SetNotBefore(notBefore); - certificateGenerator.SetNotAfter(notAfter); - - // The subject's public key goes in the certificate. - certificateGenerator.SetPublicKey(subjectKeyPair.Public); - - AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber); - AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair); - //AddBasicConstraints(certificateGenerator, isCertificateAuthority); - - if (usages != null && usages.Any()) - AddExtendedKeyUsage(certificateGenerator, usages); - - if (subjectAlternativeNames != null && subjectAlternativeNames.Any()) - AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames); - - // Set the signature algorithm. This is used to generate the thumbprint which is then signed - // with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong. - const string signatureAlgorithm = "SHA256WithRSA"; - - // The certificate is signed with the issuer's private key. - ISignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private, random); - var certificate = certificateGenerator.Generate(signatureFactory); - return certificate; - } - - /// - /// The certificate needs a serial number. This is used for revocation, - /// and usually should be an incrementing index (which makes it easier to revoke a range of certificates). - /// Since we don't have anywhere to store the incrementing index, we can just use a random number. - /// - /// - /// - private BigInteger GenerateSerialNumber(SecureRandom random) - { - var serialNumber = - BigIntegers.CreateRandomInRange( - BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random); - return serialNumber; - } - - /// - /// Generate a key pair. - /// - /// The random number generator. - /// The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days. - /// - private AsymmetricCipherKeyPair GenerateKeyPair(SecureRandom random, int strength) - { - var keyGenerationParameters = new KeyGenerationParameters(random, strength); - - var keyPairGenerator = new RsaKeyPairGenerator(); - keyPairGenerator.Init(keyGenerationParameters); - var subjectKeyPair = keyPairGenerator.GenerateKeyPair(); - return subjectKeyPair; - } - - /// - /// Add the Authority Key Identifier. According to http://www.alvestrand.no/objectid/2.5.29.35.html, this - /// identifies the public key to be used to verify the signature on this certificate. - /// In a certificate chain, this corresponds to the "Subject Key Identifier" on the *issuer* certificate. - /// The Bouncy Castle documentation, at http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation, - /// shows how to create this from the issuing certificate. Since we're creating a self-signed certificate, we have to do this slightly differently. - /// - /// - /// - /// - /// - private void AddAuthorityKeyIdentifier(X509V3CertificateGenerator certificateGenerator, - X509Name issuerDN, - AsymmetricCipherKeyPair issuerKeyPair, - BigInteger issuerSerialNumber) - { - var authorityKeyIdentifierExtension = - new AuthorityKeyIdentifier( - SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public), - new GeneralNames(new GeneralName(issuerDN)), - issuerSerialNumber); - certificateGenerator.AddExtension( - X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension); - } - - /// - /// Add the "Subject Alternative Names" extension. Note that you have to repeat - /// the value from the "Subject Name" property. - /// - /// - /// - private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator, - IEnumerable subjectAlternativeNames) - { - var subjectAlternativeNamesExtension = - new DerSequence( - subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name)) - .ToArray()); - certificateGenerator.AddExtension( - X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension); - } - - /// - /// Add the "Extended Key Usage" extension, specifying (for example) "server authentication". - /// - /// - /// - private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages) - { - certificateGenerator.AddExtension( - X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages)); - } - - /// - /// Add the "Basic Constraints" extension. - /// - /// - /// - private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator, - bool isCertificateAuthority) - { - certificateGenerator.AddExtension( - X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority)); - } - - /// - /// Add the Subject Key Identifier. - /// - /// - /// - private void AddSubjectKeyIdentifier(X509V3CertificateGenerator certificateGenerator, - AsymmetricCipherKeyPair subjectKeyPair) - { - var subjectKeyIdentifierExtension = - new SubjectKeyIdentifier( - SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(subjectKeyPair.Public)); - certificateGenerator.AddExtension( - X509Extensions.SubjectKeyIdentifier.Id, false, subjectKeyIdentifierExtension); - } - - private X509Certificate2 ConvertCertificate(X509Certificate certificate, - AsymmetricCipherKeyPair subjectKeyPair, - SecureRandom random) - { - // Now to convert the Bouncy Castle certificate to a .NET certificate. - // See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx - // ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and private key to that. - var store = new Pkcs12StoreBuilder().Build(); - - // What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name". - string friendlyName = certificate.SubjectDN.ToString(); - - // Add the certificate. - var certificateEntry = new X509CertificateEntry(certificate); - store.SetCertificateEntry(friendlyName, certificateEntry); - - // Add the private key. - store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry }); - - // Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream. - // It needs a password. Since we'll remove this later, it doesn't particularly matter what we use. - - var stream = new MemoryStream(); - store.Save(stream, CertificatePassword.ToCharArray(), random); - - var convertedCertificate = - new X509Certificate2(stream.ToArray(), - CertificatePassword, - X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); - return convertedCertificate; - } - - /// - /// WriteCertificate method - /// - public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName) - { - // This password is the one attached to the PFX file. Use 'null' for no password. - // Create PFX (PKCS #12) with private key try { - var pfx = certificate.Export(X509ContentType.Pfx, CertificatePassword); - File.WriteAllBytes(string.Format("{0}.pfx", Path.Combine(outputDirectory, certName)), pfx); + var contents = string.Format("-----BEGIN CERTIFICATE-----\r\n{0}\r\n-----END CERTIFICATE-----", Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); + writer.Write(contents); } catch (Exception ex) { - CrestronConsole.PrintLine(string.Format("Failed to write x509 cert pfx\r\n{0}", ex.Message)); + CrestronConsole.PrintLine(string.Format("Failed to write x509 cert cer\r\n{0}", ex.Message)); } - // Create Base 64 encoded CER (public key only) - using (var writer = new StreamWriter($"{Path.Combine(outputDirectory, certName)}.cer", false)) - { - try - { - var contents = string.Format("-----BEGIN CERTIFICATE-----\r\n{0}\r\n-----END CERTIFICATE-----", Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)); - writer.Write(contents); - } - catch (Exception ex) - { - CrestronConsole.PrintLine(string.Format("Failed to write x509 cert cer\r\n{0}", ex.Message)); - } - } - } - /// - /// AddCertToStore method - /// - public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl) - { - bool bRet = false; - - try - { - var store = new System.Security.Cryptography.X509Certificates.X509Store(st, sl); - store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadWrite); - store.Add(cert); - - store.Close(); - bRet = true; - } - catch (Exception ex) - { - CrestronConsole.PrintLine(string.Format("AddCertToStore Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace)); - } - - return bRet; } } + public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl) + { + bool bRet = false; + + try + { + var store = new System.Security.Cryptography.X509Certificates.X509Store(st, sl); + store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadWrite); + store.Add(cert); + + store.Close(); + bRet = true; + } + catch (Exception ex) + { + CrestronConsole.PrintLine(string.Format("AddCertToStore Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace)); + } + + return bRet; + } } \ No newline at end of file diff --git a/src/PepperDash.Core/Web/RequestHandlers/DefaultRequestHandler.cs b/src/PepperDash.Core/Web/RequestHandlers/DefaultRequestHandler.cs index 154dc01e..2fee0a62 100644 --- a/src/PepperDash.Core/Web/RequestHandlers/DefaultRequestHandler.cs +++ b/src/PepperDash.Core/Web/RequestHandlers/DefaultRequestHandler.cs @@ -1,10 +1,10 @@ using Crestron.SimplSharp.WebScripting; -namespace PepperDash.Core.Web.RequestHandlers -{ - /// - /// Represents a DefaultRequestHandler - /// +namespace PepperDash.Core.Web.RequestHandlers; + + /// + /// Web API default request handler + /// public class DefaultRequestHandler : WebApiBaseRequestHandler { /// @@ -13,5 +13,4 @@ namespace PepperDash.Core.Web.RequestHandlers public DefaultRequestHandler() : base(true) { } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs b/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs index 927092db..e8b3e85b 100644 --- a/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs +++ b/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs @@ -3,164 +3,160 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -namespace PepperDash.Core.Web.RequestHandlers +namespace PepperDash.Core.Web.RequestHandlers; + +public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler { - public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler + private readonly Dictionary> _handlers; + protected readonly bool EnableCors; + + /// + /// Constructor + /// + protected WebApiBaseRequestAsyncHandler(bool enableCors) { - private readonly Dictionary> _handlers; - protected readonly bool EnableCors; + EnableCors = enableCors; - /// - /// Constructor - /// - protected WebApiBaseRequestAsyncHandler(bool enableCors) + _handlers = new Dictionary> { - EnableCors = enableCors; + {"CONNECT", HandleConnect}, + {"DELETE", HandleDelete}, + {"GET", HandleGet}, + {"HEAD", HandleHead}, + {"OPTIONS", HandleOptions}, + {"PATCH", HandlePatch}, + {"POST", HandlePost}, + {"PUT", HandlePut}, + {"TRACE", HandleTrace} + }; + } - _handlers = new Dictionary> - { - {"CONNECT", HandleConnect}, - {"DELETE", HandleDelete}, - {"GET", HandleGet}, - {"HEAD", HandleHead}, - {"OPTIONS", HandleOptions}, - {"PATCH", HandlePatch}, - {"POST", HandlePost}, - {"PUT", HandlePut}, - {"TRACE", HandleTrace} - }; + /// + /// Constructor + /// + protected WebApiBaseRequestAsyncHandler() + : this(false) + { + } + + /// + /// Handles CONNECT method requests + /// + /// + protected virtual async Task HandleConnect(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles DELETE method requests + /// + /// + protected virtual async Task HandleDelete(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles GET method requests + /// + /// + protected virtual async Task HandleGet(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles HEAD method requests + /// + /// + protected virtual async Task HandleHead(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles OPTIONS method requests + /// + /// + protected virtual async Task HandleOptions(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles PATCH method requests + /// + /// + protected virtual async Task HandlePatch(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles POST method requests + /// + /// + protected virtual async Task HandlePost(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles PUT method requests + /// + /// + protected virtual async Task HandlePut(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles TRACE method requests + /// + /// + protected virtual async Task HandleTrace(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Process request + /// + /// + public void ProcessRequest(HttpCwsContext context) + { + if (!_handlers.TryGetValue(context.Request.HttpMethod, out Func handler)) + { + return; } - /// - /// Constructor - /// - protected WebApiBaseRequestAsyncHandler() - : this(false) + if (EnableCors) { + context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); + context.Response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); } - /// - /// Handles CONNECT method requests - /// - /// - protected virtual async Task HandleConnect(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } + var handlerTask = handler(context); - /// - /// Handles DELETE method requests - /// - /// - protected virtual async Task HandleDelete(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles GET method requests - /// - /// - protected virtual async Task HandleGet(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles HEAD method requests - /// - /// - protected virtual async Task HandleHead(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles OPTIONS method requests - /// - /// - protected virtual async Task HandleOptions(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles PATCH method requests - /// - /// - protected virtual async Task HandlePatch(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles POST method requests - /// - /// - protected virtual async Task HandlePost(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles PUT method requests - /// - /// - protected virtual async Task HandlePut(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles TRACE method requests - /// - /// - protected virtual async Task HandleTrace(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Process request - /// - /// - /// - /// ProcessRequest method - /// - public void ProcessRequest(HttpCwsContext context) - { - if (!_handlers.TryGetValue(context.Request.HttpMethod, out Func handler)) - { - return; - } - - if (EnableCors) - { - context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); - context.Response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); - } - - var handlerTask = handler(context); - - handlerTask.GetAwaiter().GetResult(); - } + handlerTask.GetAwaiter().GetResult(); } } diff --git a/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs b/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs index a02f3497..0d36a863 100644 --- a/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs +++ b/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using Crestron.SimplSharp.WebScripting; -namespace PepperDash.Core.Web.RequestHandlers -{ +namespace PepperDash.Core.Web.RequestHandlers; + /// /// CWS Base Handler, implements IHttpCwsHandler /// @@ -164,5 +164,4 @@ namespace PepperDash.Core.Web.RequestHandlers handler(context); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs index 42bc555b..4b7c1c30 100644 --- a/src/PepperDash.Core/Web/WebApiServer.cs +++ b/src/PepperDash.Core/Web/WebApiServer.cs @@ -10,8 +10,8 @@ using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Core.Web -{ +namespace PepperDash.Core.Web; + /// /// Web API server /// @@ -286,5 +286,4 @@ namespace PepperDash.Core.Web Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException); } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/WebApi/Presets/Preset.cs b/src/PepperDash.Core/WebApi/Presets/Preset.cs index 88df890c..8d174b78 100644 --- a/src/PepperDash.Core/WebApi/Presets/Preset.cs +++ b/src/PepperDash.Core/WebApi/Presets/Preset.cs @@ -1,45 +1,45 @@ using System; -namespace PepperDash.Core.WebApi.Presets -{ - /// - /// Represents a Preset - /// +namespace PepperDash.Core.WebApi.Presets; + +/// +/// Represents a preset +/// public class Preset { - /// - /// ID of preset - /// + /// + /// ID of preset + /// public int Id { get; set; } - /// - /// Gets or sets the UserId - /// + /// + /// User ID + /// public int UserId { get; set; } - /// - /// Gets or sets the RoomTypeId - /// + /// + /// Room Type ID + /// public int RoomTypeId { get; set; } - /// - /// Gets or sets the PresetName - /// + /// + /// Preset Name + /// public string PresetName { get; set; } - /// - /// Gets or sets the PresetNumber - /// + /// + /// Preset Number + /// public int PresetNumber { get; set; } - /// - /// Gets or sets the Data - /// + /// + /// Preset Data + /// public string Data { get; set; } - /// - /// Constructor - /// + /// + /// Constructor + /// public Preset() { PresetName = ""; @@ -53,35 +53,34 @@ namespace PepperDash.Core.WebApi.Presets /// public class PresetReceivedEventArgs : EventArgs { - /// - /// True when the preset is found - /// - public bool LookupSuccess { get; private set; } - - /// - /// Gets or sets the ULookupSuccess - /// - public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } } + /// + /// True when the preset is found + /// + public bool LookupSuccess { get; private set; } + + /// + /// S+ helper + /// + public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } } - /// - /// Gets or sets the Preset - /// - public Preset Preset { get; private set; } + /// + /// The preset + /// + public Preset Preset { get; private set; } /// /// For Simpl+ /// public PresetReceivedEventArgs() { } - /// - /// Constructor - /// - /// - /// + /// + /// Constructor + /// + /// + /// public PresetReceivedEventArgs(Preset preset, bool success) { - LookupSuccess = success; + LookupSuccess = success; Preset = preset; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/WebApi/Presets/User.cs b/src/PepperDash.Core/WebApi/Presets/User.cs index 4044ae7b..a20803e4 100644 --- a/src/PepperDash.Core/WebApi/Presets/User.cs +++ b/src/PepperDash.Core/WebApi/Presets/User.cs @@ -4,31 +4,31 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Core.WebApi.Presets -{ +namespace PepperDash.Core.WebApi.Presets; + /// /// /// public class User { - /// - /// - /// + /// + /// + /// public int Id { get; set; } - /// - /// Gets or sets the ExternalId - /// + /// + /// + /// public string ExternalId { get; set; } - /// - /// Gets or sets the FirstName - /// + /// + /// + /// public string FirstName { get; set; } - /// - /// Gets or sets the LastName - /// + /// + /// + /// public string LastName { get; set; } } @@ -38,19 +38,19 @@ namespace PepperDash.Core.WebApi.Presets /// public class UserReceivedEventArgs : EventArgs { - /// - /// True when user is found - /// - public bool LookupSuccess { get; private set; } + /// + /// True when user is found + /// + public bool LookupSuccess { get; private set; } - /// - /// Gets or sets the ULookupSuccess - /// - public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } } + /// + /// For stupid S+ + /// + public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } } - /// - /// Gets or sets the User - /// + /// + /// + /// public User User { get; private set; } /// @@ -58,14 +58,14 @@ namespace PepperDash.Core.WebApi.Presets /// public UserReceivedEventArgs() { } - /// - /// Constructor - /// - /// - /// + /// + /// Constructor + /// + /// + /// public UserReceivedEventArgs(User user, bool success) { - LookupSuccess = success; + LookupSuccess = success; User = user; } } @@ -75,19 +75,18 @@ namespace PepperDash.Core.WebApi.Presets /// public class UserAndRoomMessage { - /// - /// - /// + /// + /// + /// public int UserId { get; set; } - /// - /// Gets or sets the RoomTypeId - /// + /// + /// + /// public int RoomTypeId { get; set; } - /// - /// Gets or sets the PresetNumber - /// + /// + /// + /// public int PresetNumber { get; set; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs index 831d2d02..32e63cda 100644 --- a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs +++ b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs @@ -10,26 +10,26 @@ using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using PepperDash.Core.JsonToSimpl; -namespace PepperDash.Core.WebApi.Presets -{ - /// - /// Passcode client for the WebApi - /// +namespace PepperDash.Core.WebApi.Presets; + +/// +/// Passcode client for the WebApi +/// public class WebApiPasscodeClient : IKeyed { - /// - /// Notifies when user received - /// + /// + /// Notifies when user received + /// public event EventHandler UserReceived; - /// - /// Notifies when Preset received - /// + /// + /// Notifies when Preset received + /// public event EventHandler PresetReceived; - /// - /// Gets or sets the Key - /// + /// + /// Unique identifier for this instance + /// public string Key { get; private set; } //string JsonMasterKey; @@ -56,13 +56,13 @@ namespace PepperDash.Core.WebApi.Presets { } - /// - /// Initializes the instance - /// - /// - /// - /// - /// + /// + /// Initializes the instance + /// + /// + /// + /// + /// public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath) { Key = key; @@ -75,44 +75,41 @@ namespace PepperDash.Core.WebApi.Presets J2SMaster.Initialize(jsonMasterKey); } - /// - /// Gets the user for a passcode - /// - /// - /// - /// GetUserForPasscode method - /// + /// + /// Gets the user for a passcode + /// + /// public void GetUserForPasscode(string passcode) { - // Bullshit duplicate code here... These two cases should be the same - // except for https/http and the certificate ignores - if (!UrlBase.StartsWith("https")) - return; - var req = new HttpsClientRequest(); - req.Url = new UrlParser(UrlBase + "/api/users/dopin"); - req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post; - req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json")); - req.Header.AddHeader(new HttpsHeader("Accept", "application/json")); - var jo = new JObject(); - jo.Add("pin", passcode); - req.ContentString = jo.ToString(); + // Bullshit duplicate code here... These two cases should be the same + // except for https/http and the certificate ignores + if (!UrlBase.StartsWith("https")) + return; + var req = new HttpsClientRequest(); + req.Url = new UrlParser(UrlBase + "/api/users/dopin"); + req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post; + req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json")); + req.Header.AddHeader(new HttpsHeader("Accept", "application/json")); + var jo = new JObject(); + jo.Add("pin", passcode); + req.ContentString = jo.ToString(); - var client = new HttpsClient(); - client.HostVerification = false; - client.PeerVerification = false; - var resp = client.Dispatch(req); - var handler = UserReceived; - if (resp.Code == 200) - { - //CrestronConsole.PrintLine("Received: {0}", resp.ContentString); - var user = JsonConvert.DeserializeObject(resp.ContentString); - CurrentUser = user; - if (handler != null) - UserReceived(this, new UserReceivedEventArgs(user, true)); - } - else - if (handler != null) - UserReceived(this, new UserReceivedEventArgs(null, false)); + var client = new HttpsClient(); + client.HostVerification = false; + client.PeerVerification = false; + var resp = client.Dispatch(req); + var handler = UserReceived; + if (resp.Code == 200) + { + //CrestronConsole.PrintLine("Received: {0}", resp.ContentString); + var user = JsonConvert.DeserializeObject(resp.ContentString); + CurrentUser = user; + if (handler != null) + UserReceived(this, new UserReceivedEventArgs(user, true)); + } + else + if (handler != null) + UserReceived(this, new UserReceivedEventArgs(null, false)); } /// @@ -138,57 +135,57 @@ namespace PepperDash.Core.WebApi.Presets PresetNumber = presetNumber }; - var handler = PresetReceived; + var handler = PresetReceived; try { - if (!UrlBase.StartsWith("https")) - return; - var req = new HttpsClientRequest(); - req.Url = new UrlParser(UrlBase + "/api/presets/userandroom"); - req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post; - req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json")); - req.Header.AddHeader(new HttpsHeader("Accept", "application/json")); - req.ContentString = JsonConvert.SerializeObject(msg); + if (!UrlBase.StartsWith("https")) + return; + var req = new HttpsClientRequest(); + req.Url = new UrlParser(UrlBase + "/api/presets/userandroom"); + req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post; + req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json")); + req.Header.AddHeader(new HttpsHeader("Accept", "application/json")); + req.ContentString = JsonConvert.SerializeObject(msg); - var client = new HttpsClient(); - client.HostVerification = false; - client.PeerVerification = false; + var client = new HttpsClient(); + client.HostVerification = false; + client.PeerVerification = false; - // ask for the preset - var resp = client.Dispatch(req); - if (resp.Code == 200) // got it + // ask for the preset + var resp = client.Dispatch(req); + if (resp.Code == 200) // got it + { + //Debug.Console(1, this, "Received: {0}", resp.ContentString); + var preset = JsonConvert.DeserializeObject(resp.ContentString); + CurrentPreset = preset; + + //if there's no preset data, load the template + if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0) { - //Debug.Console(1, this, "Received: {0}", resp.ContentString); - var preset = JsonConvert.DeserializeObject(resp.ContentString); - CurrentPreset = preset; - - //if there's no preset data, load the template - if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0) - { - //Debug.Console(1, this, "Loaded preset has no data. Loading default template."); - LoadDefaultPresetData(); - return; - } - - J2SMaster.LoadWithJson(preset.Data); - if (handler != null) - PresetReceived(this, new PresetReceivedEventArgs(preset, true)); - } - else // no existing preset - { - CurrentPreset = new Preset(); + //Debug.Console(1, this, "Loaded preset has no data. Loading default template."); LoadDefaultPresetData(); - if (handler != null) - PresetReceived(this, new PresetReceivedEventArgs(null, false)); + return; } + + J2SMaster.LoadWithJson(preset.Data); + if (handler != null) + PresetReceived(this, new PresetReceivedEventArgs(preset, true)); + } + else // no existing preset + { + CurrentPreset = new Preset(); + LoadDefaultPresetData(); + if (handler != null) + PresetReceived(this, new PresetReceivedEventArgs(null, false)); + } } catch (HttpException e) { var resp = e.Response; Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code); LoadDefaultPresetData(); - if (handler != null) - PresetReceived(this, new PresetReceivedEventArgs(null, false)); + if (handler != null) + PresetReceived(this, new PresetReceivedEventArgs(null, false)); } } @@ -247,8 +244,8 @@ namespace PepperDash.Core.WebApi.Presets { CurrentPreset.Data = json; - if (!UrlBase.StartsWith("https")) - return; + if (!UrlBase.StartsWith("https")) + return; var req = new HttpsClientRequest(); req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post; req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase)); @@ -257,8 +254,8 @@ namespace PepperDash.Core.WebApi.Presets req.ContentString = JsonConvert.SerializeObject(CurrentPreset); var client = new HttpsClient(); - client.HostVerification = false; - client.PeerVerification = false; + client.HostVerification = false; + client.PeerVerification = false; try { var resp = client.Dispatch(req); @@ -281,4 +278,3 @@ namespace PepperDash.Core.WebApi.Presets } } } -} diff --git a/src/PepperDash.Core/XSigUtility/Serialization/IXSigSerialization.cs b/src/PepperDash.Core/XSigUtility/Serialization/IXSigSerialization.cs index 8303731e..b1dcae6b 100644 --- a/src/PepperDash.Core/XSigUtility/Serialization/IXSigSerialization.cs +++ b/src/PepperDash.Core/XSigUtility/Serialization/IXSigSerialization.cs @@ -1,25 +1,24 @@ using System.Collections.Generic; using PepperDash.Core.Intersystem.Tokens; -namespace PepperDash.Core.Intersystem.Serialization +namespace PepperDash.Core.Intersystem.Serialization; + +/// +/// Interface to determine XSig serialization for an object. +/// +public interface IXSigSerialization { /// - /// Interface to determine XSig serialization for an object. + /// Serialize the sig data /// - public interface IXSigSerialization - { - /// - /// Serialize the sig data - /// - /// - IEnumerable Serialize(); + /// + IEnumerable Serialize(); - /// - /// Deserialize the sig data - /// - /// - /// - /// - T Deserialize(IEnumerable tokens) where T : class, IXSigSerialization; - } + /// + /// Deserialize the sig data + /// + /// + /// + /// + T Deserialize(IEnumerable tokens) where T : class, IXSigSerialization; } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/Serialization/XSigSerializationException.cs b/src/PepperDash.Core/XSigUtility/Serialization/XSigSerializationException.cs index 8f3fc047..4db0e970 100644 --- a/src/PepperDash.Core/XSigUtility/Serialization/XSigSerializationException.cs +++ b/src/PepperDash.Core/XSigUtility/Serialization/XSigSerializationException.cs @@ -1,28 +1,27 @@ using System; -namespace PepperDash.Core.Intersystem.Serialization +namespace PepperDash.Core.Intersystem.Serialization; + +/// +/// Class to handle this specific exception type +/// +public class XSigSerializationException : Exception { /// - /// Class to handle this specific exception type + /// default constructor /// - public class XSigSerializationException : Exception - { - /// - /// default constructor - /// - public XSigSerializationException() { } + public XSigSerializationException() { } - /// - /// constructor with message - /// - /// - public XSigSerializationException(string message) : base(message) { } + /// + /// constructor with message + /// + /// + public XSigSerializationException(string message) : base(message) { } - /// - /// constructor with message and innner exception - /// - /// - /// - public XSigSerializationException(string message, Exception inner) : base(message, inner) { } - } + /// + /// constructor with message and innner exception + /// + /// + /// + public XSigSerializationException(string message, Exception inner) : base(message, inner) { } } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigAnalogToken.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigAnalogToken.cs index 68c61d90..b5cb2e68 100644 --- a/src/PepperDash.Core/XSigUtility/Tokens/XSigAnalogToken.cs +++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigAnalogToken.cs @@ -1,98 +1,87 @@ using System; -namespace PepperDash.Core.Intersystem.Tokens +namespace PepperDash.Core.Intersystem.Tokens; + +/// +/// Represents an XSigAnalogToken +/// +public sealed class XSigAnalogToken : XSigToken, IFormattable { + private readonly ushort _value; + /// - /// Represents an XSigAnalogToken + /// Constructor /// - public sealed class XSigAnalogToken : XSigToken, IFormattable + /// + /// + public XSigAnalogToken(int index, ushort value) + : base(index) { - private readonly ushort _value; + // 10-bits available for analog encoded data + if (index >= 1024 || index < 0) + throw new ArgumentOutOfRangeException("index"); - /// - /// Constructor - /// - /// - /// - public XSigAnalogToken(int index, ushort value) - : base(index) - { - // 10-bits available for analog encoded data - if (index >= 1024 || index < 0) - throw new ArgumentOutOfRangeException("index"); + _value = value; + } - _value = value; - } + /// + /// + /// + public ushort Value + { + get { return _value; } + } - /// - /// - /// - public ushort Value - { - get { return _value; } - } + /// + /// + /// + public override XSigTokenType TokenType + { + get { return XSigTokenType.Analog; } + } - /// - /// - /// - public override XSigTokenType TokenType - { - get { return XSigTokenType.Analog; } - } + /// + /// + /// + /// + public override byte[] GetBytes() + { + return new[] { + (byte)(0xC0 | ((Value & 0xC000) >> 10) | (Index - 1 >> 7)), + (byte)((Index - 1) & 0x7F), + (byte)((Value & 0x3F80) >> 7), + (byte)(Value & 0x7F) + }; + } - /// - /// - /// - /// - public override byte[] GetBytes() - { - return new[] { - (byte)(0xC0 | ((Value & 0xC000) >> 10) | (Index - 1 >> 7)), - (byte)((Index - 1) & 0x7F), - (byte)((Value & 0x3F80) >> 7), - (byte)(Value & 0x7F) - }; - } + /// + /// + /// + /// + /// + public override XSigToken GetTokenWithOffset(int offset) + { + if (offset == 0) return this; + return new XSigAnalogToken(Index + offset, Value); + } - /// - /// - /// - /// - /// - /// - /// GetTokenWithOffset method - /// - public override XSigToken GetTokenWithOffset(int offset) - { - if (offset == 0) return this; - return new XSigAnalogToken(Index + offset, Value); - } + /// + /// + /// + /// + public override string ToString() + { + return Index + " = 0x" + Value.ToString("X4"); + } - /// - /// - /// - /// - /// - /// ToString method - /// - /// - public override string ToString() - { - return Index + " = 0x" + Value.ToString("X4"); - } - - /// - /// - /// - /// - /// - /// - /// - /// ToString method - /// - public string ToString(string format, IFormatProvider formatProvider) - { - return Value.ToString(format, formatProvider); - } + /// + /// + /// + /// + /// + /// + public string ToString(string format, IFormatProvider formatProvider) + { + return Value.ToString(format, formatProvider); } } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigDigitalToken.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigDigitalToken.cs index 50fac7fc..a82d0eb2 100644 --- a/src/PepperDash.Core/XSigUtility/Tokens/XSigDigitalToken.cs +++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigDigitalToken.cs @@ -1,95 +1,84 @@ using System; -namespace PepperDash.Core.Intersystem.Tokens +namespace PepperDash.Core.Intersystem.Tokens; + +/// +/// Represents an XSigDigitalToken +/// +public sealed class XSigDigitalToken : XSigToken { + private readonly bool _value; + /// - /// Represents an XSigDigitalToken + /// /// - public sealed class XSigDigitalToken : XSigToken + /// + /// + public XSigDigitalToken(int index, bool value) + : base(index) { - private readonly bool _value; + // 12-bits available for digital encoded data + if (index >= 4096 || index < 0) + throw new ArgumentOutOfRangeException("index"); - /// - /// - /// - /// - /// - public XSigDigitalToken(int index, bool value) - : base(index) - { - // 12-bits available for digital encoded data - if (index >= 4096 || index < 0) - throw new ArgumentOutOfRangeException("index"); + _value = value; + } - _value = value; - } + /// + /// + /// + public bool Value + { + get { return _value; } + } - /// - /// - /// - public bool Value - { - get { return _value; } - } + /// + /// + /// + public override XSigTokenType TokenType + { + get { return XSigTokenType.Digital; } + } - /// - /// - /// - public override XSigTokenType TokenType - { - get { return XSigTokenType.Digital; } - } + /// + /// + /// + /// + public override byte[] GetBytes() + { + return new[] { + (byte)(0x80 | (Value ? 0 : 0x20) | ((Index - 1) >> 7)), + (byte)((Index - 1) & 0x7F) + }; + } - /// - /// - /// - /// - public override byte[] GetBytes() - { - return new[] { - (byte)(0x80 | (Value ? 0 : 0x20) | ((Index - 1) >> 7)), - (byte)((Index - 1) & 0x7F) - }; - } + /// + /// + /// + /// + /// + public override XSigToken GetTokenWithOffset(int offset) + { + if (offset == 0) return this; + return new XSigDigitalToken(Index + offset, Value); + } - /// - /// - /// - /// - /// - /// - /// GetTokenWithOffset method - /// - public override XSigToken GetTokenWithOffset(int offset) - { - if (offset == 0) return this; - return new XSigDigitalToken(Index + offset, Value); - } + /// + /// + /// + /// + public override string ToString() + { + return Index + " = " + (Value ? "High" : "Low"); + } - /// - /// - /// - /// - /// - /// ToString method - /// - /// - public override string ToString() - { - return Index + " = " + (Value ? "High" : "Low"); - } - - /// - /// - /// - /// - /// - /// - /// ToString method - /// - public string ToString(IFormatProvider formatProvider) - { - return Value.ToString(formatProvider); - } + /// + /// + /// + /// + /// + public string ToString(IFormatProvider formatProvider) + { + return Value.ToString(formatProvider); } } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigSerialToken.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigSerialToken.cs index 635d40e3..3b6a3f5f 100644 --- a/src/PepperDash.Core/XSigUtility/Tokens/XSigSerialToken.cs +++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigSerialToken.cs @@ -1,88 +1,80 @@ using System; using System.Text; -namespace PepperDash.Core.Intersystem.Tokens +namespace PepperDash.Core.Intersystem.Tokens; + +/// +/// Represents an XSigSerialToken +/// +public sealed class XSigSerialToken : XSigToken { + private readonly string _value; + /// - /// Represents an XSigSerialToken + /// Constructor /// - public sealed class XSigSerialToken : XSigToken + /// + /// + public XSigSerialToken(int index, string value) + : base(index) { - private readonly string _value; + // 10-bits available for serial encoded data + if (index >= 1024 || index < 0) + throw new ArgumentOutOfRangeException("index"); - /// - /// Constructor - /// - /// - /// - public XSigSerialToken(int index, string value) - : base(index) - { - // 10-bits available for serial encoded data - if (index >= 1024 || index < 0) - throw new ArgumentOutOfRangeException("index"); + _value = value; + } - _value = value; - } + /// + /// + /// + public string Value + { + get { return _value; } + } - /// - /// - /// - public string Value - { - get { return _value; } - } + /// + /// + /// + public override XSigTokenType TokenType + { + get { return XSigTokenType.Serial; } + } - /// - /// - /// - public override XSigTokenType TokenType - { - get { return XSigTokenType.Serial; } - } + /// + /// + /// + /// + public override byte[] GetBytes() + { + var serialBytes = String.IsNullOrEmpty(Value) ? new byte[0] : Encoding.GetEncoding(28591).GetBytes(Value); + + var xsig = new byte[serialBytes.Length + 3]; + xsig[0] = (byte)(0xC8 | (Index - 1 >> 7)); + xsig[1] = (byte)((Index - 1) & 0x7F); + xsig[xsig.Length - 1] = 0xFF; - /// - /// - /// - /// - public override byte[] GetBytes() - { - var serialBytes = String.IsNullOrEmpty(Value) ? new byte[0] : Encoding.GetEncoding(28591).GetBytes(Value); - - var xsig = new byte[serialBytes.Length + 3]; - xsig[0] = (byte)(0xC8 | (Index - 1 >> 7)); - xsig[1] = (byte)((Index - 1) & 0x7F); - xsig[xsig.Length - 1] = 0xFF; + Buffer.BlockCopy(serialBytes, 0, xsig, 2, serialBytes.Length); + return xsig; + } - Buffer.BlockCopy(serialBytes, 0, xsig, 2, serialBytes.Length); - return xsig; - } + /// + /// + /// + /// + /// + public override XSigToken GetTokenWithOffset(int offset) + { + if (offset == 0) return this; + return new XSigSerialToken(Index + offset, Value); + } - /// - /// - /// - /// - /// - /// - /// GetTokenWithOffset method - /// - public override XSigToken GetTokenWithOffset(int offset) - { - if (offset == 0) return this; - return new XSigSerialToken(Index + offset, Value); - } - - /// - /// - /// - /// - /// - /// ToString method - /// - /// - public override string ToString() - { - return Index + " = \"" + Value + "\""; - } + /// + /// + /// + /// + public override string ToString() + { + return Index + " = \"" + Value + "\""; } } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigToken.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigToken.cs index 4c00a2ed..cd706bd4 100644 --- a/src/PepperDash.Core/XSigUtility/Tokens/XSigToken.cs +++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigToken.cs @@ -1,45 +1,44 @@ -namespace PepperDash.Core.Intersystem.Tokens +namespace PepperDash.Core.Intersystem.Tokens; + +/// +/// Represents the base class for all XSig datatypes. +/// +public abstract class XSigToken { + private readonly int _index; + /// - /// Represents the base class for all XSig datatypes. + /// Constructs an XSigToken with the specified index. /// - public abstract class XSigToken + /// Index for the data. + protected XSigToken(int index) { - private readonly int _index; - - /// - /// Constructs an XSigToken with the specified index. - /// - /// Index for the data. - protected XSigToken(int index) - { - _index = index; - } - - /// - /// XSig 1-based index. - /// - public int Index - { - get { return _index; } - } - - /// - /// XSigToken type. - /// - public abstract XSigTokenType TokenType { get; } - - /// - /// Generates the XSig bytes for the corresponding token. - /// - /// XSig byte array. - public abstract byte[] GetBytes(); - - /// - /// Returns a new token if necessary with an updated index based on the specified offset. - /// - /// Offset to adjust the index with. - /// XSigToken - public abstract XSigToken GetTokenWithOffset(int offset); + _index = index; } + + /// + /// XSig 1-based index. + /// + public int Index + { + get { return _index; } + } + + /// + /// XSigToken type. + /// + public abstract XSigTokenType TokenType { get; } + + /// + /// Generates the XSig bytes for the corresponding token. + /// + /// XSig byte array. + public abstract byte[] GetBytes(); + + /// + /// Returns a new token if necessary with an updated index based on the specified offset. + /// + /// Offset to adjust the index with. + /// XSigToken + public abstract XSigToken GetTokenWithOffset(int offset); } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigTokenType.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigTokenType.cs index 26d6c123..60641b4f 100644 --- a/src/PepperDash.Core/XSigUtility/Tokens/XSigTokenType.cs +++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigTokenType.cs @@ -1,23 +1,22 @@ -namespace PepperDash.Core.Intersystem.Tokens +namespace PepperDash.Core.Intersystem.Tokens; + +/// +/// XSig token types. +/// +public enum XSigTokenType { /// - /// XSig token types. + /// Digital signal datatype. /// - public enum XSigTokenType - { - /// - /// Digital signal datatype. - /// - Digital, + Digital, - /// - /// Analog signal datatype. - /// - Analog, + /// + /// Analog signal datatype. + /// + Analog, - /// - /// Serial signal datatype. - /// - Serial - } + /// + /// Serial signal datatype. + /// + Serial } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/XSigHelpers.cs b/src/PepperDash.Core/XSigUtility/XSigHelpers.cs index d93cc32b..4f970be2 100644 --- a/src/PepperDash.Core/XSigUtility/XSigHelpers.cs +++ b/src/PepperDash.Core/XSigUtility/XSigHelpers.cs @@ -18,264 +18,221 @@ using PepperDash.Core.Intersystem.Tokens; 11111111 <- denotes end of data */ -namespace PepperDash.Core.Intersystem +namespace PepperDash.Core.Intersystem; + +/// +/// Helper methods for creating XSig byte sequences compatible with the Intersystem Communications (ISC) symbol. +/// +/// +/// Indexing is not from the start of each signal type but rather from the beginning of the first defined signal +/// the Intersystem Communications (ISC) symbol. +/// +public static class XSigHelpers { /// - /// Helper methods for creating XSig byte sequences compatible with the Intersystem Communications (ISC) symbol. + /// Forces all outputs to 0. /// - /// - /// Indexing is not from the start of each signal type but rather from the beginning of the first defined signal - /// the Intersystem Communications (ISC) symbol. - /// - public static class XSigHelpers + /// Bytes in XSig format for clear outputs trigger. + public static byte[] ClearOutputs() { - /// - /// Forces all outputs to 0. - /// - /// Bytes in XSig format for clear outputs trigger. - public static byte[] ClearOutputs() + return new byte[] { 0xFC }; + } + + /// + /// Evaluate all inputs and re-transmit any digital, analog, and permanent serail signals not set to 0. + /// + /// Bytes in XSig format for send status trigger. + public static byte[] SendStatus() + { + return new byte[] { 0xFD }; + } + + /// + /// Get bytes for an IXSigStateResolver object. + /// + /// XSig state resolver. + /// Bytes in XSig format for each token within the state representation. + public static byte[] GetBytes(IXSigSerialization xSigSerialization) + { + return GetBytes(xSigSerialization, 0); + } + + /// + /// Get bytes for an IXSigStateResolver object, with a specified offset. + /// + /// XSig state resolver. + /// Offset to which the data will be aligned. + /// Bytes in XSig format for each token within the state representation. + public static byte[] GetBytes(IXSigSerialization xSigSerialization, int offset) + { + var tokens = xSigSerialization.Serialize(); + if (tokens == null) return new byte[0]; + using (var memoryStream = new MemoryStream()) { - return new byte[] { 0xFC }; - } + using (var tokenWriter = new XSigTokenStreamWriter(memoryStream)) + tokenWriter.WriteXSigData(xSigSerialization, offset); - /// - /// Evaluate all inputs and re-transmit any digital, analog, and permanent serail signals not set to 0. - /// - /// Bytes in XSig format for send status trigger. - public static byte[] SendStatus() - { - return new byte[] { 0xFD }; - } - - /// - /// Get bytes for an IXSigStateResolver object. - /// - /// XSig state resolver. - /// Bytes in XSig format for each token within the state representation. - /// - /// GetBytes method - /// - public static byte[] GetBytes(IXSigSerialization xSigSerialization) - { - return GetBytes(xSigSerialization, 0); - } - - /// - /// Get bytes for an IXSigStateResolver object, with a specified offset. - /// - /// XSig state resolver. - /// Offset to which the data will be aligned. - /// Bytes in XSig format for each token within the state representation. - /// - /// GetBytes method - /// - public static byte[] GetBytes(IXSigSerialization xSigSerialization, int offset) - { - var tokens = xSigSerialization.Serialize(); - if (tokens == null) return new byte[0]; - using (var memoryStream = new MemoryStream()) - { - using (var tokenWriter = new XSigTokenStreamWriter(memoryStream)) - tokenWriter.WriteXSigData(xSigSerialization, offset); - - return memoryStream.ToArray(); - } - } - - /// - /// Get bytes for a single digital signal. - /// - /// 1-based digital index - /// Digital data to be encoded - /// Bytes in XSig format for digtial information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int index, bool value) - { - return GetBytes(index, 0, value); - } - - /// - /// Get bytes for a single digital signal. - /// - /// 1-based digital index - /// Index offset. - /// Digital data to be encoded - /// Bytes in XSig format for digtial information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int index, int offset, bool value) - { - return new XSigDigitalToken(index + offset, value).GetBytes(); - } - - /// - /// Get byte sequence for multiple digital signals. - /// - /// Starting index of the sequence. - /// Digital signal value array. - /// Byte sequence in XSig format for digital signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int startIndex, bool[] values) - { - return GetBytes(startIndex, 0, values); - } - - /// - /// Get byte sequence for multiple digital signals. - /// - /// Starting index of the sequence. - /// Index offset. - /// Digital signal value array. - /// Byte sequence in XSig format for digital signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int startIndex, int offset, bool[] values) - { - // Digital XSig data is 2 bytes per value - const int fixedLength = 2; - var bytes = new byte[values.Length * fixedLength]; - for (var i = 0; i < values.Length; i++) - Buffer.BlockCopy(GetBytes(startIndex++, offset, values[i]), 0, bytes, i * fixedLength, fixedLength); - - return bytes; - } - - /// - /// Get bytes for a single analog signal. - /// - /// 1-based analog index - /// Analog data to be encoded - /// Bytes in XSig format for analog signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int index, ushort value) - { - return GetBytes(index, 0, value); - } - - /// - /// Get bytes for a single analog signal. - /// - /// 1-based analog index - /// Index offset. - /// Analog data to be encoded - /// Bytes in XSig format for analog signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int index, int offset, ushort value) - { - return new XSigAnalogToken(index + offset, value).GetBytes(); - } - - /// - /// Get byte sequence for multiple analog signals. - /// - /// Starting index of the sequence. - /// Analog signal value array. - /// Byte sequence in XSig format for analog signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int startIndex, ushort[] values) - { - return GetBytes(startIndex, 0, values); - } - - /// - /// Get byte sequence for multiple analog signals. - /// - /// Starting index of the sequence. - /// Index offset. - /// Analog signal value array. - /// Byte sequence in XSig format for analog signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int startIndex, int offset, ushort[] values) - { - // Analog XSig data is 4 bytes per value - const int fixedLength = 4; - var bytes = new byte[values.Length * fixedLength]; - for (var i = 0; i < values.Length; i++) - Buffer.BlockCopy(GetBytes(startIndex++, offset, values[i]), 0, bytes, i * fixedLength, fixedLength); - - return bytes; - } - - /// - /// Get bytes for a single serial signal. - /// - /// 1-based serial index - /// Serial data to be encoded - /// Bytes in XSig format for serial signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int index, string value) - { - return GetBytes(index, 0, value); - } - - /// - /// Get bytes for a single serial signal. - /// - /// 1-based serial index - /// Index offset. - /// Serial data to be encoded - /// Bytes in XSig format for serial signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int index, int offset, string value) - { - return new XSigSerialToken(index + offset, value).GetBytes(); - } - - /// - /// Get byte sequence for multiple serial signals. - /// - /// Starting index of the sequence. - /// Serial signal value array. - /// Byte sequence in XSig format for serial signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int startIndex, string[] values) - { - return GetBytes(startIndex, 0, values); - } - - /// - /// Get byte sequence for multiple serial signals. - /// - /// Starting index of the sequence. - /// Index offset. - /// Serial signal value array. - /// Byte sequence in XSig format for serial signal information. - /// - /// GetBytes method - /// - public static byte[] GetBytes(int startIndex, int offset, string[] values) - { - // Serial XSig data is not fixed-length like the other formats - var dstOffset = 0; - var bytes = new byte[values.Sum(v => v.Length + 3)]; - for (var i = 0; i < values.Length; i++) - { - var data = GetBytes(startIndex++, offset, values[i]); - Buffer.BlockCopy(data, 0, bytes, dstOffset, data.Length); - dstOffset += data.Length; - } - - return bytes; + return memoryStream.ToArray(); } } + + /// + /// Get bytes for a single digital signal. + /// + /// 1-based digital index + /// Digital data to be encoded + /// Bytes in XSig format for digtial information. + public static byte[] GetBytes(int index, bool value) + { + return GetBytes(index, 0, value); + } + + /// + /// Get bytes for a single digital signal. + /// + /// 1-based digital index + /// Index offset. + /// Digital data to be encoded + /// Bytes in XSig format for digtial information. + public static byte[] GetBytes(int index, int offset, bool value) + { + return new XSigDigitalToken(index + offset, value).GetBytes(); + } + + /// + /// Get byte sequence for multiple digital signals. + /// + /// Starting index of the sequence. + /// Digital signal value array. + /// Byte sequence in XSig format for digital signal information. + public static byte[] GetBytes(int startIndex, bool[] values) + { + return GetBytes(startIndex, 0, values); + } + + /// + /// Get byte sequence for multiple digital signals. + /// + /// Starting index of the sequence. + /// Index offset. + /// Digital signal value array. + /// Byte sequence in XSig format for digital signal information. + public static byte[] GetBytes(int startIndex, int offset, bool[] values) + { + // Digital XSig data is 2 bytes per value + const int fixedLength = 2; + var bytes = new byte[values.Length * fixedLength]; + for (var i = 0; i < values.Length; i++) + Buffer.BlockCopy(GetBytes(startIndex++, offset, values[i]), 0, bytes, i * fixedLength, fixedLength); + + return bytes; + } + + /// + /// Get bytes for a single analog signal. + /// + /// 1-based analog index + /// Analog data to be encoded + /// Bytes in XSig format for analog signal information. + public static byte[] GetBytes(int index, ushort value) + { + return GetBytes(index, 0, value); + } + + /// + /// Get bytes for a single analog signal. + /// + /// 1-based analog index + /// Index offset. + /// Analog data to be encoded + /// Bytes in XSig format for analog signal information. + public static byte[] GetBytes(int index, int offset, ushort value) + { + return new XSigAnalogToken(index + offset, value).GetBytes(); + } + + /// + /// Get byte sequence for multiple analog signals. + /// + /// Starting index of the sequence. + /// Analog signal value array. + /// Byte sequence in XSig format for analog signal information. + public static byte[] GetBytes(int startIndex, ushort[] values) + { + return GetBytes(startIndex, 0, values); + } + + /// + /// Get byte sequence for multiple analog signals. + /// + /// Starting index of the sequence. + /// Index offset. + /// Analog signal value array. + /// Byte sequence in XSig format for analog signal information. + public static byte[] GetBytes(int startIndex, int offset, ushort[] values) + { + // Analog XSig data is 4 bytes per value + const int fixedLength = 4; + var bytes = new byte[values.Length * fixedLength]; + for (var i = 0; i < values.Length; i++) + Buffer.BlockCopy(GetBytes(startIndex++, offset, values[i]), 0, bytes, i * fixedLength, fixedLength); + + return bytes; + } + + /// + /// Get bytes for a single serial signal. + /// + /// 1-based serial index + /// Serial data to be encoded + /// Bytes in XSig format for serial signal information. + public static byte[] GetBytes(int index, string value) + { + return GetBytes(index, 0, value); + } + + /// + /// Get bytes for a single serial signal. + /// + /// 1-based serial index + /// Index offset. + /// Serial data to be encoded + /// Bytes in XSig format for serial signal information. + public static byte[] GetBytes(int index, int offset, string value) + { + return new XSigSerialToken(index + offset, value).GetBytes(); + } + + /// + /// Get byte sequence for multiple serial signals. + /// + /// Starting index of the sequence. + /// Serial signal value array. + /// Byte sequence in XSig format for serial signal information. + public static byte[] GetBytes(int startIndex, string[] values) + { + return GetBytes(startIndex, 0, values); + } + + /// + /// Get byte sequence for multiple serial signals. + /// + /// Starting index of the sequence. + /// Index offset. + /// Serial signal value array. + /// Byte sequence in XSig format for serial signal information. + public static byte[] GetBytes(int startIndex, int offset, string[] values) + { + // Serial XSig data is not fixed-length like the other formats + var dstOffset = 0; + var bytes = new byte[values.Sum(v => v.Length + 3)]; + for (var i = 0; i < values.Length; i++) + { + var data = GetBytes(startIndex++, offset, values[i]); + Buffer.BlockCopy(data, 0, bytes, dstOffset, data.Length); + dstOffset += data.Length; + } + + return bytes; + } } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/XSigTokenStreamReader.cs b/src/PepperDash.Core/XSigUtility/XSigTokenStreamReader.cs index 03e79946..3c222960 100644 --- a/src/PepperDash.Core/XSigUtility/XSigTokenStreamReader.cs +++ b/src/PepperDash.Core/XSigUtility/XSigTokenStreamReader.cs @@ -4,153 +4,143 @@ using Crestron.SimplSharp.CrestronIO; using PepperDash.Core.Intersystem.Serialization; using PepperDash.Core.Intersystem.Tokens; -namespace PepperDash.Core.Intersystem +namespace PepperDash.Core.Intersystem; + +/// +/// XSigToken stream reader. +/// +public sealed class XSigTokenStreamReader : IDisposable { + private readonly Stream _stream; + private readonly bool _leaveOpen; + + /// /// - /// XSigToken stream reader. + /// XSigToken stream reader constructor. /// - public sealed class XSigTokenStreamReader : IDisposable + /// Input stream to read from. + /// Stream is null. + /// Stream cannot be read from. + public XSigTokenStreamReader(Stream stream) + : this(stream, false) { } + + /// + /// XSigToken stream reader constructor. + /// + /// Input stream to read from. + /// Determines whether to leave the stream open or not. + /// Stream is null. + /// Stream cannot be read from. + public XSigTokenStreamReader(Stream stream, bool leaveOpen) { - private readonly Stream _stream; - private readonly bool _leaveOpen; + if (stream == null) + throw new ArgumentNullException("stream"); + if (!stream.CanRead) + throw new ArgumentException("The specified stream cannot be read from."); - /// - /// - /// XSigToken stream reader constructor. - /// - /// Input stream to read from. - /// Stream is null. - /// Stream cannot be read from. - public XSigTokenStreamReader(Stream stream) - : this(stream, false) { } + _stream = stream; + _leaveOpen = leaveOpen; + } - /// - /// XSigToken stream reader constructor. - /// - /// Input stream to read from. - /// Determines whether to leave the stream open or not. - /// Stream is null. - /// Stream cannot be read from. - public XSigTokenStreamReader(Stream stream, bool leaveOpen) + /// + /// Reads a 16-bit unsigned integer from the specified stream using Big Endian byte order. + /// + /// Input stream + /// Result + /// True if successful, otherwise false. + public static bool TryReadUInt16BE(Stream stream, out ushort value) + { + value = 0; + if (stream.Length < 2) + return false; + + var buffer = new byte[2]; + stream.Read(buffer, 0, 2); + value = (ushort)((buffer[0] << 8) | buffer[1]); + return true; + } + + /// + /// Read XSig token from the stream. + /// + /// XSigToken + /// Offset is less than 0. + public XSigToken ReadXSigToken() + { + ushort prefix; + if (!TryReadUInt16BE(_stream, out prefix)) + return null; + + if ((prefix & 0xF880) == 0xC800) // Serial data { - if (stream == null) - throw new ArgumentNullException("stream"); - if (!stream.CanRead) - throw new ArgumentException("The specified stream cannot be read from."); + var index = ((prefix & 0x0700) >> 1) | (prefix & 0x7F); + var n = 0; + const int maxSerialDataLength = 252; + var chars = new char[maxSerialDataLength]; + int ch; + while ((ch = _stream.ReadByte()) != 0xFF) + { + if (ch == -1) // Reached end of stream without end of data marker + return null; + + chars[n++] = (char)ch; + } - _stream = stream; - _leaveOpen = leaveOpen; + return new XSigSerialToken((ushort)(index + 1), new string(chars, 0, n)); } - /// - /// Reads a 16-bit unsigned integer from the specified stream using Big Endian byte order. - /// - /// Input stream - /// Result - /// True if successful, otherwise false. - /// - /// TryReadUInt16BE method - /// - public static bool TryReadUInt16BE(Stream stream, out ushort value) + if ((prefix & 0xC880) == 0xC000) // Analog data { - value = 0; - if (stream.Length < 2) - return false; - - var buffer = new byte[2]; - stream.Read(buffer, 0, 2); - value = (ushort)((buffer[0] << 8) | buffer[1]); - return true; - } - - /// - /// Read XSig token from the stream. - /// - /// XSigToken - /// Offset is less than 0. - /// - /// ReadXSigToken method - /// - public XSigToken ReadXSigToken() - { - ushort prefix; - if (!TryReadUInt16BE(_stream, out prefix)) + ushort data; + if (!TryReadUInt16BE(_stream, out data)) return null; - if ((prefix & 0xF880) == 0xC800) // Serial data - { - var index = ((prefix & 0x0700) >> 1) | (prefix & 0x7F); - var n = 0; - const int maxSerialDataLength = 252; - var chars = new char[maxSerialDataLength]; - int ch; - while ((ch = _stream.ReadByte()) != 0xFF) - { - if (ch == -1) // Reached end of stream without end of data marker - return null; - - chars[n++] = (char)ch; - } - - return new XSigSerialToken((ushort)(index + 1), new string(chars, 0, n)); - } - - if ((prefix & 0xC880) == 0xC000) // Analog data - { - ushort data; - if (!TryReadUInt16BE(_stream, out data)) - return null; - - var index = ((prefix & 0x0700) >> 1) | (prefix & 0x7F); - var value = ((prefix & 0x3000) << 2) | ((data & 0x7F00) >> 1) | (data & 0x7F); - return new XSigAnalogToken((ushort)(index + 1), (ushort)value); - } - - if ((prefix & 0xC080) == 0x8000) // Digital data - { - var index = ((prefix & 0x1F00) >> 1) | (prefix & 0x7F); - var value = (prefix & 0x2000) == 0; - return new XSigDigitalToken((ushort)(index + 1), value); - } - - return null; + var index = ((prefix & 0x0700) >> 1) | (prefix & 0x7F); + var value = ((prefix & 0x3000) << 2) | ((data & 0x7F00) >> 1) | (data & 0x7F); + return new XSigAnalogToken((ushort)(index + 1), (ushort)value); } - /// - /// Reads all available XSig tokens from the stream. - /// - /// XSigToken collection. - /// - /// ReadAllXSigTokens method - /// - public IEnumerable ReadAllXSigTokens() + if ((prefix & 0xC080) == 0x8000) // Digital data { - var tokens = new List(); - XSigToken token; - while ((token = ReadXSigToken()) != null) - tokens.Add(token); - - return tokens; + var index = ((prefix & 0x1F00) >> 1) | (prefix & 0x7F); + var value = (prefix & 0x2000) == 0; + return new XSigDigitalToken((ushort)(index + 1), value); } - /// - /// Attempts to deserialize all XSig data within the stream from the current position. - /// - /// Type to deserialize the information to. - /// Deserialized object. - public T DeserializeStream() - where T : class, IXSigSerialization, new() - { - return new T().Deserialize(ReadAllXSigTokens()); - } + return null; + } - /// - /// Disposes of the internal stream if specified to not leave open. - /// - public void Dispose() - { - if (!_leaveOpen) - _stream.Dispose(); - } + /// + /// Reads all available XSig tokens from the stream. + /// + /// XSigToken collection. + public IEnumerable ReadAllXSigTokens() + { + var tokens = new List(); + XSigToken token; + while ((token = ReadXSigToken()) != null) + tokens.Add(token); + + return tokens; + } + + /// + /// Attempts to deserialize all XSig data within the stream from the current position. + /// + /// Type to deserialize the information to. + /// Deserialized object. + public T DeserializeStream() + where T : class, IXSigSerialization, new() + { + return new T().Deserialize(ReadAllXSigTokens()); + } + + /// + /// Disposes of the internal stream if specified to not leave open. + /// + public void Dispose() + { + if (!_leaveOpen) + _stream.Dispose(); } } \ No newline at end of file diff --git a/src/PepperDash.Core/XSigUtility/XSigTokenStreamWriter.cs b/src/PepperDash.Core/XSigUtility/XSigTokenStreamWriter.cs index 12eeaf91..da973acd 100644 --- a/src/PepperDash.Core/XSigUtility/XSigTokenStreamWriter.cs +++ b/src/PepperDash.Core/XSigUtility/XSigTokenStreamWriter.cs @@ -5,147 +5,131 @@ using Crestron.SimplSharp.CrestronIO; using PepperDash.Core.Intersystem.Serialization; using PepperDash.Core.Intersystem.Tokens; -namespace PepperDash.Core.Intersystem +namespace PepperDash.Core.Intersystem; + +/// +/// XSigToken stream writer. +/// +public sealed class XSigTokenStreamWriter : IDisposable { + private readonly Stream _stream; + private readonly bool _leaveOpen; + + /// /// - /// XSigToken stream writer. + /// XSigToken stream writer constructor. /// - public sealed class XSigTokenStreamWriter : IDisposable + /// Input stream to write to. + /// Stream is null. + /// Stream cannot be written to. + public XSigTokenStreamWriter(Stream stream) + : this(stream, false) { } + + /// + /// XSigToken stream writer constructor. + /// + /// Input stream to write to. + /// Determines whether to leave the stream open or not. + /// Stream is null. + /// Stream cannot be written to. + public XSigTokenStreamWriter(Stream stream, bool leaveOpen) { - private readonly Stream _stream; - private readonly bool _leaveOpen; + if (stream == null) + throw new ArgumentNullException("stream"); + if (!stream.CanWrite) + throw new ArgumentException("The specified stream cannot be written to."); - /// - /// - /// XSigToken stream writer constructor. - /// - /// Input stream to write to. - /// Stream is null. - /// Stream cannot be written to. - public XSigTokenStreamWriter(Stream stream) - : this(stream, false) { } + _stream = stream; + _leaveOpen = leaveOpen; + } - /// - /// XSigToken stream writer constructor. - /// - /// Input stream to write to. - /// Determines whether to leave the stream open or not. - /// Stream is null. - /// Stream cannot be written to. - public XSigTokenStreamWriter(Stream stream, bool leaveOpen) + /// + /// Write XSig data gathered from an IXSigStateResolver to the stream. + /// + /// IXSigStateResolver object. + public void WriteXSigData(IXSigSerialization xSigSerialization) + { + WriteXSigData(xSigSerialization, 0); + } + + /// + /// Write XSig data gathered from an IXSigStateResolver to the stream. + /// + /// IXSigStateResolver object. + /// Index offset for each XSigToken. + public void WriteXSigData(IXSigSerialization xSigSerialization, int offset) + { + if (xSigSerialization == null) + throw new ArgumentNullException("xSigSerialization"); + + var tokens = xSigSerialization.Serialize(); + WriteXSigData(tokens, offset); + } + + /// + /// Write XSigToken to the stream. + /// + /// XSigToken object. + public void WriteXSigData(XSigToken token) + { + WriteXSigData(token, 0); + } + + /// + /// Write XSigToken to the stream. + /// + /// XSigToken object. + /// Index offset for each XSigToken. + public void WriteXSigData(XSigToken token, int offset) + { + WriteXSigData(new[] { token }, offset); + } + + /// + /// Writes an array of XSigTokens to the stream. + /// + /// XSigToken objects. + public void WriteXSigData(XSigToken[] tokens) + { + WriteXSigData(tokens.AsEnumerable()); + } + + /// + /// Write an enumerable collection of XSigTokens to the stream. + /// + /// XSigToken objects. + public void WriteXSigData(IEnumerable tokens) + { + WriteXSigData(tokens, 0); + } + + /// + /// Write an enumerable collection of XSigTokens to the stream. + /// + /// XSigToken objects. + /// Index offset for each XSigToken. + public void WriteXSigData(IEnumerable tokens, int offset) + { + if (offset < 0) + throw new ArgumentOutOfRangeException("offset", "Offset must be greater than or equal to 0."); + + if (tokens != null) { - if (stream == null) - throw new ArgumentNullException("stream"); - if (!stream.CanWrite) - throw new ArgumentException("The specified stream cannot be written to."); - - _stream = stream; - _leaveOpen = leaveOpen; - } - - /// - /// Write XSig data gathered from an IXSigStateResolver to the stream. - /// - /// IXSigStateResolver object. - /// - /// WriteXSigData method - /// - public void WriteXSigData(IXSigSerialization xSigSerialization) - { - WriteXSigData(xSigSerialization, 0); - } - - /// - /// Write XSig data gathered from an IXSigStateResolver to the stream. - /// - /// IXSigStateResolver object. - /// Index offset for each XSigToken. - /// - /// WriteXSigData method - /// - public void WriteXSigData(IXSigSerialization xSigSerialization, int offset) - { - if (xSigSerialization == null) - throw new ArgumentNullException("xSigSerialization"); - - var tokens = xSigSerialization.Serialize(); - WriteXSigData(tokens, offset); - } - - /// - /// Write XSigToken to the stream. - /// - /// XSigToken object. - /// - /// WriteXSigData method - /// - public void WriteXSigData(XSigToken token) - { - WriteXSigData(token, 0); - } - - /// - /// Write XSigToken to the stream. - /// - /// XSigToken object. - /// Index offset for each XSigToken. - /// - /// WriteXSigData method - /// - public void WriteXSigData(XSigToken token, int offset) - { - WriteXSigData(new[] { token }, offset); - } - - /// - /// Writes an array of XSigTokens to the stream. - /// - /// XSigToken objects. - public void WriteXSigData(XSigToken[] tokens) - { - WriteXSigData(tokens.AsEnumerable()); - } - - /// - /// Write an enumerable collection of XSigTokens to the stream. - /// - /// XSigToken objects. - public void WriteXSigData(IEnumerable tokens) - { - WriteXSigData(tokens, 0); - } - - /// - /// Write an enumerable collection of XSigTokens to the stream. - /// - /// XSigToken objects. - /// Index offset for each XSigToken. - /// - /// WriteXSigData method - /// - public void WriteXSigData(IEnumerable tokens, int offset) - { - if (offset < 0) - throw new ArgumentOutOfRangeException("offset", "Offset must be greater than or equal to 0."); - - if (tokens != null) + foreach (var token in tokens) { - foreach (var token in tokens) - { - if (token == null) continue; - var bytes = token.GetTokenWithOffset(offset).GetBytes(); - _stream.Write(bytes, 0, bytes.Length); - } + if (token == null) continue; + var bytes = token.GetTokenWithOffset(offset).GetBytes(); + _stream.Write(bytes, 0, bytes.Length); } } + } - /// - /// Dispose method - /// - public void Dispose() - { - if (!_leaveOpen) - _stream.Dispose(); - } + /// + /// Disposes of the internal stream if specified to not leave open. + /// + public void Dispose() + { + if (!_leaveOpen) + _stream.Dispose(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/BridgeBase.cs b/src/PepperDash.Essentials.Core/Bridges/BridgeBase.cs index 070f1743..4982a642 100644 --- a/src/PepperDash.Essentials.Core/Bridges/BridgeBase.cs +++ b/src/PepperDash.Essentials.Core/Bridges/BridgeBase.cs @@ -13,507 +13,408 @@ using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +/// +/// Base class for bridge API variants +/// +public abstract class BridgeApi : EssentialsDevice { - /// - /// Base class for bridge API variants - /// - [Obsolete("Will be removed in v3.0.0")] - public abstract class BridgeApi : EssentialsDevice + protected BridgeApi(string key) : + base(key) { - /// - /// Constructor - /// - /// Device key - protected BridgeApi(string key) : - base(key) - { + } +} + +/// +/// Bridge API using EISC +/// +public class EiscApiAdvanced : BridgeApi, ICommunicationMonitor +{ + public EiscApiPropertiesConfig PropertiesConfig { get; private set; } + + public Dictionary JoinMaps { get; private set; } + + public BasicTriList Eisc { get; private set; } + + public EiscApiAdvanced(DeviceConfig dc, BasicTriList eisc) : + base(dc.Key) + { + JoinMaps = new Dictionary(); + + PropertiesConfig = dc.Properties.ToObject(); + //PropertiesConfig = JsonConvert.DeserializeObject(dc.Properties.ToString()); + + Eisc = eisc; + + Eisc.SigChange += Eisc_SigChange; + + CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, Eisc, 120000, 300000); + + AddPostActivationAction(LinkDevices); + AddPostActivationAction(LinkRooms); + AddPostActivationAction(RegisterEisc); + } + + public override bool CustomActivate() + { + CommunicationMonitor.Start(); + return base.CustomActivate(); + } + + public override bool Deactivate() + { + CommunicationMonitor.Stop(); + return base.Deactivate(); + } + + private void LinkDevices() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Linking Devices..."); + + if (PropertiesConfig.Devices == null) + { + Debug.LogMessage(LogEventLevel.Debug, this, "No devices linked to this bridge"); + return; + } + + foreach (var d in PropertiesConfig.Devices) + { + var device = DeviceManager.GetDeviceForKey(d.DeviceKey); + + if (device == null) + { + continue; + } + + Debug.LogMessage(LogEventLevel.Debug, this, "Linking Device: '{0}'", device.Key); + + if (device is IBridgeAdvanced bridge) + { + bridge.LinkToApi(Eisc, d.JoinStart, d.JoinMapKey, this); + continue; + } + + Debug.LogMessage(LogEventLevel.Information, this, + "{0} is not compatible with this bridge type. Please use 'eiscapi' instead, or updae the device.", + device.Key); + } + } + + private void RegisterEisc() + { + if (Eisc.Registered) + { + return; + } + + var registerResult = Eisc.Register(); + + if (registerResult != eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Registration result: {0}", registerResult); + return; + } + + Debug.LogMessage(LogEventLevel.Debug, this, "EISC registration successful"); + } + + public void LinkRooms() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Linking Rooms..."); + + if (PropertiesConfig.Rooms == null) + { + Debug.LogMessage(LogEventLevel.Debug, this, "No rooms linked to this bridge."); + return; + } + + foreach (var room in PropertiesConfig.Rooms) + { + var rm = DeviceManager.GetDeviceForKey(room.RoomKey) as IBridgeAdvanced; + + if (rm == null) + { + Debug.LogMessage(LogEventLevel.Debug, this, + "Room {0} does not implement IBridgeAdvanced. Skipping...", room.RoomKey); + continue; + } + + rm.LinkToApi(Eisc, room.JoinStart, room.JoinMapKey, this); } } /// - /// Class to link devices and rooms to an EISC Instance + /// Adds a join map /// - public class EiscApiAdvanced : BridgeApi, ICommunicationMonitor + /// + /// + public void AddJoinMap(string deviceKey, JoinMapBaseAdvanced joinMap) { - /// - /// Gets the PropertiesConfig - /// - public EiscApiPropertiesConfig PropertiesConfig { get; private set; } - - /// - /// Gets the JoinMaps dictionary - /// - public Dictionary JoinMaps { get; private set; } - - /// - /// Gets the EISC instance - /// - public BasicTriList Eisc { get; private set; } - - /// - /// Constructor - /// - /// Device configuration - /// EISC instance - public EiscApiAdvanced(DeviceConfig dc, BasicTriList eisc) : - base(dc.Key) + if (!JoinMaps.ContainsKey(deviceKey)) { - JoinMaps = new Dictionary(); - - PropertiesConfig = dc.Properties.ToObject(); - - Eisc = eisc; - - Eisc.SigChange += Eisc_SigChange; - - CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, Eisc, 120000, 300000); - - AddPostActivationAction(LinkDevices); - AddPostActivationAction(LinkRooms); - AddPostActivationAction(RegisterEisc); + JoinMaps.Add(deviceKey, joinMap); } - - /// - /// CustomActivate method - /// - public override bool CustomActivate() + else { - CommunicationMonitor.Start(); - return base.CustomActivate(); + Debug.LogMessage(LogEventLevel.Verbose, this, "Unable to add join map with key '{0}'. Key already exists in JoinMaps dictionary", deviceKey); } - - /// - /// Deactivate method - /// - public override bool Deactivate() - { - CommunicationMonitor.Stop(); - return base.Deactivate(); - } - - private void LinkDevices() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Linking Devices..."); - - if (PropertiesConfig.Devices == null) - { - this.LogDebug("No devices linked to this bridge"); - return; - } - - foreach (var d in PropertiesConfig.Devices) - { - var device = DeviceManager.GetDeviceForKey(d.DeviceKey); - - if (device == null) - { - continue; - } - - Debug.LogMessage(LogEventLevel.Debug, this, "Linking Device: '{0}'", device.Key); - - if (device is IBridgeAdvanced bridge) - { - bridge.LinkToApi(Eisc, d.JoinStart, d.JoinMapKey, this); - continue; - } - - this.LogWarning("{deviceKey} is not compatible with this bridge type. Please update the device.", device.Key); - } - } - - private void RegisterEisc() - { - if (Eisc.Registered) - { - return; - } - - var registerResult = Eisc.Register(); - - if (registerResult != eDeviceRegistrationUnRegistrationResponse.Success) - { - this.LogVerbose("Registration result: {registerResult}", registerResult); - return; - } - - this.LogDebug("EISC registration successful"); - } - - /// - /// Link rooms to this EISC. Rooms MUST implement IBridgeAdvanced - /// - public void LinkRooms() - { - this.LogDebug("Linking Rooms..."); - - if (PropertiesConfig.Rooms == null) - { - this.LogDebug("No rooms linked to this bridge."); - return; - } - - foreach (var room in PropertiesConfig.Rooms) - { - if (!(DeviceManager.GetDeviceForKey(room.RoomKey) is IBridgeAdvanced rm)) - { - this.LogDebug("Room {roomKey} does not implement IBridgeAdvanced. Skipping...", room.RoomKey); - continue; - } - - rm.LinkToApi(Eisc, room.JoinStart, room.JoinMapKey, this); - } - } - - /// - /// Adds a join map - /// - /// The key of the device to add the join map for - /// The join map to add - public void AddJoinMap(string deviceKey, JoinMapBaseAdvanced joinMap) - { - if (!JoinMaps.ContainsKey(deviceKey)) - { - JoinMaps.Add(deviceKey, joinMap); - } - else - { - this.LogWarning("Unable to add join map with key '{deviceKey}'. Key already exists in JoinMaps dictionary", deviceKey); - } - } - - /// - /// PrintJoinMaps method - /// - public virtual void PrintJoinMaps() - { - CrestronConsole.ConsoleCommandResponse("Join Maps for EISC IPID: {0}\r\n", Eisc.ID.ToString("X")); - - foreach (var joinMap in JoinMaps) - { - CrestronConsole.ConsoleCommandResponse("Join map for device '{0}':", joinMap.Key); - joinMap.Value.PrintJoinMapInfo(); - } - } - - /// - /// MarkdownForBridge method - /// - public virtual void MarkdownForBridge(string bridgeKey) - { - this.LogInformation("Writing Joinmaps to files for EISC IPID: {eiscId}", Eisc.ID.ToString("X")); - - foreach (var joinMap in JoinMaps) - { - this.LogInformation("Generating markdown for device '{deviceKey}':", joinMap.Key); - joinMap.Value.MarkdownJoinMapInfo(joinMap.Key, bridgeKey); - } - } - - /// - /// Prints the join map for a device by key - /// - /// The key of the device to print the join map for - public void PrintJoinMapForDevice(string deviceKey) - { - var joinMap = JoinMaps[deviceKey]; - - if (joinMap == null) - { - this.LogInformation("Unable to find joinMap for device with key: '{deviceKey}'", deviceKey); - return; - } - - this.LogInformation("Join map for device '{deviceKey}' on EISC '{eiscKey}':", deviceKey, Key); - joinMap.PrintJoinMapInfo(); - } - /// - /// Prints the join map for a device by key in Markdown format - /// - /// The key of the device to print the join map for - /// The key of the bridge to use for the Markdown output - public void MarkdownJoinMapForDevice(string deviceKey, string bridgeKey) - { - var joinMap = JoinMaps[deviceKey]; - - if (joinMap == null) - { - this.LogInformation("Unable to find joinMap for device with key: '{deviceKey}'", deviceKey); - return; - } - - this.LogInformation("Join map for device '{deviceKey}' on EISC '{eiscKey}':", deviceKey, Key); - joinMap.MarkdownJoinMapInfo(deviceKey, bridgeKey); - } - - /// - /// Used for debugging to trigger an action based on a join number and type - /// - /// The join number to execute the action for - /// The type of join (digital, analog, serial) - /// The state to pass to the action - public void ExecuteJoinAction(uint join, string type, object state) - { - try - { - switch (type.ToLower()) - { - case "digital": - { - if (Eisc.BooleanOutput[join].UserObject is Action userObject) - { - this.LogVerbose("Executing Boolean Action"); - userObject(Convert.ToBoolean(state)); - } - else - this.LogVerbose("User Object is null. Nothing to Execute"); - break; - } - case "analog": - { - if (Eisc.UShortOutput[join].UserObject is Action userObject) - { - this.LogVerbose("Executing Analog Action"); - userObject(Convert.ToUInt16(state)); - } - else - this.LogVerbose("User Object is null. Nothing to Execute"); - break; - } - case "serial": - { - if (Eisc.StringOutput[join].UserObject is Action userObject) - { - this.LogVerbose("Executing Serial Action"); - userObject(Convert.ToString(state)); - } - else - this.LogVerbose("User Object is null. Nothing to Execute"); - break; - } - default: - { - this.LogVerbose("Unknown join type. Use digital/serial/analog"); - break; - } - } - } - catch (Exception e) - { - this.LogError("ExecuteJoinAction error: {message}", e.Message); - this.LogDebug(e, "Stack Trace: "); - } - - } - - /// - /// Handle incoming sig changes - /// - /// BasicTriList device that triggered the event - /// Event arguments containing the signal information - protected void Eisc_SigChange(object currentDevice, SigEventArgs args) - { - try - { - this.LogVerbose("EiscApiAdvanced change: {type} {number}={value}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); - var userObject = args.Sig.UserObject; - - if (userObject == null) return; - - - if (userObject is Action) - { - this.LogDebug("Executing Boolean Action"); - (userObject as Action)(args.Sig.BoolValue); - } - else if (userObject is Action) - { - this.LogDebug("Executing Analog Action"); - (userObject as Action)(args.Sig.UShortValue); - } - else if (userObject is Action) - { - this.LogDebug("Executing Serial Action"); - (userObject as Action)(args.Sig.StringValue); - } - } - catch (Exception e) - { - this.LogError("Eisc_SigChange handler error: {message}", e.Message); - this.LogDebug(e, "Stack Trace: "); - } - } - - #region Implementation of ICommunicationMonitor - - /// - /// Gets or sets the CommunicationMonitor - /// - public StatusMonitorBase CommunicationMonitor { get; private set; } - - #endregion } /// - /// Represents a EiscApiPropertiesConfig + /// Prints all the join maps on this bridge /// - public class EiscApiPropertiesConfig + public virtual void PrintJoinMaps() { - /// - /// Gets or sets the Control - /// - [JsonProperty("control")] - public EssentialsControlPropertiesConfig Control { get; set; } + CrestronConsole.ConsoleCommandResponse("Join Maps for EISC IPID: {0}\r\n", Eisc.ID.ToString("X")); - /// - /// Gets or sets the Devices - /// - [JsonProperty("devices")] - public List Devices { get; set; } - - /// - /// Gets or sets the Rooms - /// - [JsonProperty("rooms")] - public List Rooms { get; set; } - - - /// - /// Represents a ApiDevicePropertiesConfig - /// - public class ApiDevicePropertiesConfig + foreach (var joinMap in JoinMaps) { - /// - /// Gets or sets the DeviceKey - /// - [JsonProperty("deviceKey")] - public string DeviceKey { get; set; } - - /// - /// Gets or sets the JoinStart - /// - [JsonProperty("joinStart")] - public uint JoinStart { get; set; } - - /// - /// Gets or sets the JoinMapKey - /// - [JsonProperty("joinMapKey")] - public string JoinMapKey { get; set; } + CrestronConsole.ConsoleCommandResponse("Join map for device '{0}':", joinMap.Key); + joinMap.Value.PrintJoinMapInfo(); } + } + /// + /// Generates markdown for all join maps on this bridge + /// + public virtual void MarkdownForBridge(string bridgeKey) + { + Debug.LogMessage(LogEventLevel.Information, this, "Writing Joinmaps to files for EISC IPID: {0}", Eisc.ID.ToString("X")); - /// - /// Represents a ApiRoomPropertiesConfig - /// - public class ApiRoomPropertiesConfig + foreach (var joinMap in JoinMaps) { - /// - /// Gets or sets the RoomKey - /// - [JsonProperty("roomKey")] - public string RoomKey { get; set; } - - /// - /// Gets or sets the JoinStart - /// - [JsonProperty("joinStart")] - public uint JoinStart { get; set; } - - /// - /// Gets or sets the JoinMapKey - /// - [JsonProperty("joinMapKey")] - public string JoinMapKey { get; set; } + Debug.LogMessage(LogEventLevel.Information, "Generating markdown for device '{0}':", joinMap.Key); + joinMap.Value.MarkdownJoinMapInfo(joinMap.Key, bridgeKey); } - } /// - /// Factory class for EiscApiAdvanced devices + /// Prints the join map for a device by key /// - /// - /// Supported types: - /// eiscapiadv - Create a standard EISC client over TCP/IP - /// eiscapiadvanced - Create a standard EISC client over TCP/IP - /// eiscapiadvancedserver - Create an EISC server - /// eiscapiadvancedclient - Create an EISC client - /// vceiscapiadv - Create a VC-4 EISC client - /// vceiscapiadvanced - Create a VC-4 EISC client - /// eiscapiadvudp - Create a standard EISC client over UDP - /// eiscapiadvancedudp - Create a standard EISC client over UDP - /// - public class EiscApiAdvancedFactory : EssentialsDeviceFactory + /// + public void PrintJoinMapForDevice(string deviceKey) { - /// - /// Constructor - /// - public EiscApiAdvancedFactory() + var joinMap = JoinMaps[deviceKey]; + + if (joinMap == null) { - TypeNames = new List { "eiscapiadv", "eiscapiadvanced", "eiscapiadvancedserver", "eiscapiadvancedclient", "vceiscapiadv", "vceiscapiadvanced", "eiscapiadvudp", "eiscapiadvancedudp" }; + Debug.LogMessage(LogEventLevel.Information, this, "Unable to find joinMap for device with key: '{0}'", deviceKey); + return; } - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) + Debug.LogMessage(LogEventLevel.Information, "Join map for device '{0}' on EISC '{1}':", deviceKey, Key); + joinMap.PrintJoinMapInfo(); + } + /// + /// Prints the join map for a device by key + /// + /// + public void MarkdownJoinMapForDevice(string deviceKey, string bridgeKey) + { + var joinMap = JoinMaps[deviceKey]; + + if (joinMap == null) { - Debug.LogDebug("Attempting to create new EiscApiAdvanced Device"); + Debug.LogMessage(LogEventLevel.Information, this, "Unable to find joinMap for device with key: '{0}'", deviceKey); + return; + } - var controlProperties = CommFactory.GetControlPropertiesConfig(dc); + Debug.LogMessage(LogEventLevel.Information, "Join map for device '{0}' on EISC '{1}':", deviceKey, Key); + joinMap.MarkdownJoinMapInfo(deviceKey, bridgeKey); + } - BasicTriList eisc; - - switch (dc.Type.ToLower()) + /// + /// Used for debugging to trigger an action based on a join number and type + /// + /// + /// + /// + public void ExecuteJoinAction(uint join, string type, object state) + { + try + { + switch (type.ToLower()) { - case "eiscapiadvudp": - case "eiscapiadvancedudp": + case "digital": { - eisc = new EthernetIntersystemCommunications(controlProperties.IpIdInt, - controlProperties.TcpSshProperties.Address, Global.ControlSystem); - break; - } - case "eiscapiadv": - case "eiscapiadvanced": - { - eisc = new ThreeSeriesTcpIpEthernetIntersystemCommunications(controlProperties.IpIdInt, - controlProperties.TcpSshProperties.Address, Global.ControlSystem); - break; - } - case "eiscapiadvancedserver": - { - eisc = new EISCServer(controlProperties.IpIdInt, Global.ControlSystem); - break; - } - case "eiscapiadvancedclient": - { - eisc = new EISCClient(controlProperties.IpIdInt, controlProperties.TcpSshProperties.Address, Global.ControlSystem); - break; - } - case "vceiscapiadv": - case "vceiscapiadvanced": - { - if (string.IsNullOrEmpty(controlProperties.RoomId)) + var uo = Eisc.BooleanOutput[join].UserObject as Action; + if (uo != null) { - Debug.LogInformation("Unable to build VC-4 EISC Client for device {deviceKey}. Room ID is missing or empty", dc.Key); - eisc = null; - break; + Debug.LogMessage(LogEventLevel.Verbose, this, "Executing Action: {0}", uo.ToString()); + uo(Convert.ToBoolean(state)); } - eisc = new VirtualControlEISCClient(controlProperties.IpIdInt, controlProperties.RoomId, - Global.ControlSystem); + else + Debug.LogMessage(LogEventLevel.Verbose, this, "User Action is null. Nothing to Execute"); + break; + } + case "analog": + { + var uo = Eisc.BooleanOutput[join].UserObject as Action; + if (uo != null) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Executing Action: {0}", uo.ToString()); + uo(Convert.ToUInt16(state)); + } + else + Debug.LogMessage(LogEventLevel.Verbose, this, "User Action is null. Nothing to Execute"); break; + } + case "serial": + { + var uo = Eisc.BooleanOutput[join].UserObject as Action; + if (uo != null) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Executing Action: {0}", uo.ToString()); + uo(Convert.ToString(state)); + } + else + Debug.LogMessage(LogEventLevel.Verbose, this, "User Action is null. Nothing to Execute"); break; } default: - eisc = null; - break; + { + Debug.LogMessage(LogEventLevel.Verbose, "Unknown join type. Use digital/serial/analog"); + break; + } } + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Error: {0}", e); + } - if (eisc == null) - { - return null; - } + } - return new EiscApiAdvanced(dc, eisc); + /// + /// Handles incoming sig changes + /// + /// + /// + protected void Eisc_SigChange(object currentDevice, SigEventArgs args) + { + try + { + Debug.LogMessage(LogEventLevel.Verbose, this, "EiscApiAdvanced change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); + var uo = args.Sig.UserObject; + + if (uo == null) return; + + Debug.LogMessage(LogEventLevel.Debug, this, "Executing Action: {0}", uo.ToString()); + 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); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Error in Eisc_SigChange handler: {0}", e); } } + #region Implementation of ICommunicationMonitor + + public StatusMonitorBase CommunicationMonitor { get; private set; } + + #endregion +} + +public class EiscApiPropertiesConfig +{ + [JsonProperty("control")] + public EssentialsControlPropertiesConfig Control { get; set; } + + [JsonProperty("devices")] + public List Devices { get; set; } + + [JsonProperty("rooms")] + public List Rooms { get; set; } + + + public class ApiDevicePropertiesConfig + { + [JsonProperty("deviceKey")] + public string DeviceKey { get; set; } + + [JsonProperty("joinStart")] + public uint JoinStart { get; set; } + + [JsonProperty("joinMapKey")] + public string JoinMapKey { get; set; } + } + + public class ApiRoomPropertiesConfig + { + [JsonProperty("roomKey")] + public string RoomKey { get; set; } + + [JsonProperty("joinStart")] + public uint JoinStart { get; set; } + + [JsonProperty("joinMapKey")] + public string JoinMapKey { get; set; } + } + +} + +public class EiscApiAdvancedFactory : EssentialsDeviceFactory +{ + public EiscApiAdvancedFactory() + { + TypeNames = new List { "eiscapiadv", "eiscapiadvanced", "eiscapiadvancedserver", "eiscapiadvancedclient", "vceiscapiadv", "vceiscapiadvanced" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new EiscApiAdvanced Device"); + + var controlProperties = CommFactory.GetControlPropertiesConfig(dc); + + BasicTriList eisc; + + switch (dc.Type.ToLower()) + { + case "eiscapiadv": + case "eiscapiadvanced": + { + eisc = new ThreeSeriesTcpIpEthernetIntersystemCommunications(controlProperties.IpIdInt, + controlProperties.TcpSshProperties.Address, Global.ControlSystem); + break; + } + case "eiscapiadvancedserver": + { + eisc = new EISCServer(controlProperties.IpIdInt, Global.ControlSystem); + break; + } + case "eiscapiadvancedclient": + { + eisc = new EISCClient(controlProperties.IpIdInt, controlProperties.TcpSshProperties.Address, Global.ControlSystem); + break; + } + case "vceiscapiadv": + case "vceiscapiadvanced": + { + if (string.IsNullOrEmpty(controlProperties.RoomId)) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to build VC-4 EISC Client for device {0}. Room ID is missing or empty", dc.Key); + eisc = null; + break; + } + eisc = new VirtualControlEISCClient(controlProperties.IpIdInt, controlProperties.RoomId, + Global.ControlSystem); + break; + } + default: + eisc = null; + break; + } + + if (eisc == null) + { + return null; + } + + return new EiscApiAdvanced(dc, eisc); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/BridgeHelper.cs b/src/PepperDash.Essentials.Core/Bridges/BridgeHelper.cs index 8d183b77..3a3ceff2 100644 --- a/src/PepperDash.Essentials.Core/Bridges/BridgeHelper.cs +++ b/src/PepperDash.Essentials.Core/Bridges/BridgeHelper.cs @@ -3,71 +3,62 @@ using Serilog.Events; //using PepperDash.Essentials.Devices.Common.Cameras; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +/// +/// Helper methods for bridges +/// +public static class BridgeHelper { - /// - /// Helper methods for bridges - /// - public static class BridgeHelper + public static void PrintJoinMap(string command) { - /// - /// PrintJoinMp method - /// - /// target bridgekey to print join map for - public static void PrintJoinMap(string command) + var targets = command.Split(' '); + + var bridgeKey = targets[0].Trim(); + + if (!(DeviceManager.GetDeviceForKey(bridgeKey) is EiscApiAdvanced bridge)) { - var targets = command.Split(' '); - - var bridgeKey = targets[0].Trim(); - - if (!(DeviceManager.GetDeviceForKey(bridgeKey) is EiscApiAdvanced bridge)) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to find advanced bridge with key: '{0}'", bridgeKey); - return; - } - - if (targets.Length > 1) - { - var deviceKey = targets[1].Trim(); - - if (string.IsNullOrEmpty(deviceKey)) return; - bridge.PrintJoinMapForDevice(deviceKey); - } - else - { - bridge.PrintJoinMaps(); - } + Debug.LogMessage(LogEventLevel.Information, "Unable to find advanced bridge with key: '{0}'", bridgeKey); + return; } - /// - /// JoinmapMarkdown method - /// - public static void JoinmapMarkdown(string command) + + if (targets.Length > 1) { - var targets = command.Split(' '); + var deviceKey = targets[1].Trim(); - var bridgeKey = targets[0].Trim(); - - var bridge = DeviceManager.GetDeviceForKey(bridgeKey) as EiscApiAdvanced; - - if (bridge == null) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to find advanced bridge with key: '{0}'", bridgeKey); - return; - } - - if (targets.Length > 1) - { - var deviceKey = targets[1].Trim(); - - if (string.IsNullOrEmpty(deviceKey)) return; - bridge.MarkdownJoinMapForDevice(deviceKey, bridgeKey); - } - else - { - bridge.MarkdownForBridge(bridgeKey); - - } + if (string.IsNullOrEmpty(deviceKey)) return; + bridge.PrintJoinMapForDevice(deviceKey); + } + else + { + bridge.PrintJoinMaps(); } } + public static void JoinmapMarkdown(string command) + { + var targets = command.Split(' '); + var bridgeKey = targets[0].Trim(); + + var bridge = DeviceManager.GetDeviceForKey(bridgeKey) as EiscApiAdvanced; + + if (bridge == null) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to find advanced bridge with key: '{0}'", bridgeKey); + return; + } + + if (targets.Length > 1) + { + var deviceKey = targets[1].Trim(); + + if (string.IsNullOrEmpty(deviceKey)) return; + bridge.MarkdownJoinMapForDevice(deviceKey, bridgeKey); + } + else + { + bridge.MarkdownForBridge(bridgeKey); + + } + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/IBridge.cs b/src/PepperDash.Essentials.Core/Bridges/IBridge.cs index 34c11171..87b4bd35 100644 --- a/src/PepperDash.Essentials.Core/Bridges/IBridge.cs +++ b/src/PepperDash.Essentials.Core/Bridges/IBridge.cs @@ -1,19 +1,11 @@ using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +/// +/// Defines a device that uses JoinMapBaseAdvanced for its join map +/// +public interface IBridgeAdvanced { - /// - /// Defines the contract for IBridgeAdvanced - /// - public interface IBridgeAdvanced - { - /// - /// Links the bridge to the API using the provided trilist, join start, join map key, and bridge. - /// - /// The trilist to link to. - /// The starting join number. - /// The key for the join map. - /// The EISC API bridge. - void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); - } + void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/AirMediaControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/AirMediaControllerJoinMap.cs index 2bd218f6..33fca20e 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/AirMediaControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/AirMediaControllerJoinMap.cs @@ -1,110 +1,70 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class AirMediaControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IsInSession")] + public JoinDataComplete IsInSession = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media In Sharing Session", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HdmiVideoSync")] + public JoinDataComplete HdmiVideoSync = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Has HDMI Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("AutomaticInputRoutingEnabled")] + public JoinDataComplete AutomaticInputRoutingEnabled = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Automatic Input Routing Enable(d)", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoOut")] + public JoinDataComplete VideoOut = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Video Route Select / Feedback", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("ErrorFB")] + public JoinDataComplete ErrorFB = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Error Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("NumberOfUsersConnectedFB")] + public JoinDataComplete NumberOfUsersConnectedFB = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Number of Users Connected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("LoginCode")] + public JoinDataComplete LoginCode = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Login Code Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Device Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("ConnectionAddressFB")] + public JoinDataComplete ConnectionAddressFB = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media IP Address", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("HostnameFB")] + public JoinDataComplete HostnameFB = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Hostname", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("SerialNumberFeedback")] + public JoinDataComplete SerialNumberFeedback = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Air Media Serial Number", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + /// - /// Represents a AirMediaControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class AirMediaControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public AirMediaControllerJoinMap(uint joinStart) + : this(joinStart, typeof(AirMediaControllerJoinMap)) { - /// - /// Air Media Online status - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Air Media In Sharing Session status - /// - [JoinName("IsInSession")] - public JoinDataComplete IsInSession = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media In Sharing Session", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Air Media Has HDMI Video Sync status - /// - [JoinName("HdmiVideoSync")] - public JoinDataComplete HdmiVideoSync = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Has HDMI Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Air Media Automatic Input Routing Enable(d) - /// - [JoinName("AutomaticInputRoutingEnabled")] - public JoinDataComplete AutomaticInputRoutingEnabled = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Automatic Input Routing Enable(d)", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Air Media Video Route Select / Feedback - /// - [JoinName("VideoOut")] - public JoinDataComplete VideoOut = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Video Route Select / Feedback", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Air Media Error Status Feedback - /// - [JoinName("ErrorFB")] - public JoinDataComplete ErrorFB = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Error Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Air Media Number of Users Connected Feedback - /// - [JoinName("NumberOfUsersConnectedFB")] - public JoinDataComplete NumberOfUsersConnectedFB = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Number of Users Connected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Air Media Login Code Set / Get - /// - [JoinName("LoginCode")] - public JoinDataComplete LoginCode = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Login Code Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Air Media Device Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Device Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Air Media IP Address Feedback - /// - [JoinName("ConnectionAddressFB")] - public JoinDataComplete ConnectionAddressFB = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media IP Address", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Air Media Hostname Feedback - /// - [JoinName("HostnameFB")] - public JoinDataComplete HostnameFB = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Hostname", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Air Media Serial Number Feedback - /// - [JoinName("SerialNumberFeedback")] - public JoinDataComplete SerialNumberFeedback = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Air Media Serial Number", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public AirMediaControllerJoinMap(uint joinStart) - : this(joinStart, typeof(AirMediaControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected AirMediaControllerJoinMap(uint joinStart, Type type) : base(joinStart, type){} } + + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected AirMediaControllerJoinMap(uint joinStart, Type type) : base(joinStart, type){} } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/AppleTvJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/AppleTvJoinMap.cs index c337f9cf..3b3eaa06 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/AppleTvJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/AppleTvJoinMap.cs @@ -1,77 +1,52 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class AppleTvJoinMap : JoinMapBaseAdvanced { + [JoinName("UpArrow")] + public JoinDataComplete UpArrow = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "AppleTv Nav Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DnArrow")] + public JoinDataComplete DnArrow = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "AppleTv Nav Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("LeftArrow")] + public JoinDataComplete LeftArrow = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "AppleTv Nav Left", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RightArrow")] + public JoinDataComplete RightArrow = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "AppleTv Nav Right", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Menu")] + public JoinDataComplete Menu = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "AppleTv Menu", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Select")] + public JoinDataComplete Select = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "AppleTv Select", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PlayPause")] + public JoinDataComplete PlayPause = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "AppleTv Play/Pause", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + /// - /// Represents a AppleTvJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class AppleTvJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public AppleTvJoinMap(uint joinStart) + : base(joinStart, typeof(AppleTvJoinMap)) { - /// - /// AppleTv Nav Up - /// - [JoinName("UpArrow")] - public JoinDataComplete UpArrow = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "AppleTv Nav Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + } - /// - /// AppleTv Nav Down - /// - [JoinName("DnArrow")] - public JoinDataComplete DnArrow = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "AppleTv Nav Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// AppleTv Nav Left - /// - [JoinName("LeftArrow")] - public JoinDataComplete LeftArrow = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "AppleTv Nav Left", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// AppleTv Nav Right - /// - [JoinName("RightArrow")] - public JoinDataComplete RightArrow = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "AppleTv Nav Right", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// AppleTv Menu - /// - [JoinName("Menu")] - public JoinDataComplete Menu = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "AppleTv Menu", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// AppleTv Select - /// - [JoinName("Select")] - public JoinDataComplete Select = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "AppleTv Select", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// AppleTv Play/Pause - /// - [JoinName("PlayPause")] - public JoinDataComplete PlayPause = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "AppleTv Play/Pause", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public AppleTvJoinMap(uint joinStart) - : base(joinStart, typeof(AppleTvJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - public AppleTvJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + public AppleTvJoinMap(uint joinStart, Type type) : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/C2nRthsControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/C2nRthsControllerJoinMap.cs index bb9da22f..17f8f4db 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/C2nRthsControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/C2nRthsControllerJoinMap.cs @@ -1,63 +1,44 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class C2nRthsControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Temp Sensor Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("TemperatureFormat")] + public JoinDataComplete TemperatureFormat = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Temp Sensor Unit Format", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Temperature")] + public JoinDataComplete Temperature = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Temp Sensor Temperature Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Humidity")] + public JoinDataComplete Humidity = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Temp Sensor Humidity Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Temp Sensor Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + /// - /// Represents a C2nRthsControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class C2nRthsControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public C2nRthsControllerJoinMap(uint joinStart) + : this(joinStart, typeof(C2nRthsControllerJoinMap)) { - /// - /// C2nRthsController Online status - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Temp Sensor Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + } - /// - /// Temperature Format (C/F) - /// - [JoinName("TemperatureFormat")] - public JoinDataComplete TemperatureFormat = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Temp Sensor Unit Format", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Temperature Sensor Feedbacks - /// - [JoinName("Temperature")] - public JoinDataComplete Temperature = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Temp Sensor Temperature Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Humidity Sensor Feedbacks - /// - [JoinName("Humidity")] - public JoinDataComplete Humidity = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Temp Sensor Humidity Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Temp Sensor Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Temp Sensor Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public C2nRthsControllerJoinMap(uint joinStart) - : this(joinStart, typeof(C2nRthsControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected C2nRthsControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected C2nRthsControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/CameraControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/CameraControllerJoinMap.cs index aad43513..0cb011fa 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/CameraControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/CameraControllerJoinMap.cs @@ -1,140 +1,68 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +/// +/// Join map for CameraBase devices +/// +public class CameraControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("TiltUp")] + public JoinDataComplete TiltUp = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, new JoinMetadata { Description = "Tilt Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("TiltDown")] + public JoinDataComplete TiltDown = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, new JoinMetadata { Description = "Tilt Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("PanLeft")] + public JoinDataComplete PanLeft = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, new JoinMetadata { Description = "Pan Left", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("PanRight")] + public JoinDataComplete PanRight = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, new JoinMetadata { Description = "Pan Right", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("ZoomIn")] + public JoinDataComplete ZoomIn = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, new JoinMetadata { Description = "Zoom In", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("ZoomOut")] + public JoinDataComplete ZoomOut = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, new JoinMetadata { Description = "Zoom Out", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, new JoinMetadata { Description = "Is Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + [JoinName("PowerOn")] + public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, new JoinMetadata { Description = "Power On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("PowerOff")] + public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, new JoinMetadata { Description = "Power Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("NumberOfPresets")] + public JoinDataComplete NumberOfPresets = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, new JoinMetadata { Description = "Tells Essentials the number of defined presets", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); + [JoinName("PresetRecallStart")] + public JoinDataComplete PresetRecallStart = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 20 }, new JoinMetadata { Description = "Preset Recall Start", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("PresetLabelStart")] + public JoinDataComplete PresetLabelStart = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 20 }, new JoinMetadata { Description = "Preset Label Start", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial }); + [JoinName("PresetSaveStart")] + public JoinDataComplete PresetSaveStart = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 20 }, new JoinMetadata { Description = "Preset Save Start", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("CameraModeAuto")] + public JoinDataComplete CameraModeAuto = new JoinDataComplete(new JoinData { JoinNumber = 51, JoinSpan = 1 }, new JoinMetadata { Description = "Camera Mode Auto", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("CameraModeManual")] + public JoinDataComplete CameraModeManual = new JoinDataComplete(new JoinData { JoinNumber = 52, JoinSpan = 1 }, new JoinMetadata { Description = "Camera Mode Manual", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("CameraModeOff")] + public JoinDataComplete CameraModeOff = new JoinDataComplete(new JoinData { JoinNumber = 53, JoinSpan = 1 }, new JoinMetadata { Description = "Camera Mode Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SupportsCameraModeAuto")] + public JoinDataComplete SupportsCameraModeAuto = new JoinDataComplete(new JoinData { JoinNumber = 55, JoinSpan = 1 }, new JoinMetadata { Description = "Supports Camera Mode Auto", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("SupportsCameraModeOff")] + public JoinDataComplete SupportsCameraModeOff = new JoinDataComplete(new JoinData { JoinNumber = 56, JoinSpan = 1 }, new JoinMetadata { Description = "Supports Camera Mode Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("SupportsPresets")] + public JoinDataComplete SupportsPresets = new JoinDataComplete(new JoinData { JoinNumber = 57, JoinSpan = 1 }, new JoinMetadata { Description = "Supports Presets", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + /// - /// Represents a CameraControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class CameraControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public CameraControllerJoinMap(uint joinStart) + : this(joinStart, typeof(CameraControllerJoinMap)) { - /// - /// Tilt Up - /// - [JoinName("TiltUp")] - public JoinDataComplete TiltUp = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, new JoinMetadata { Description = "Tilt Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Tilt Down - /// - [JoinName("TiltDown")] - public JoinDataComplete TiltDown = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, new JoinMetadata { Description = "Tilt Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Pan Left - /// - [JoinName("PanLeft")] - public JoinDataComplete PanLeft = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, new JoinMetadata { Description = "Pan Left", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Pan Right - /// - [JoinName("PanRight")] - public JoinDataComplete PanRight = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, new JoinMetadata { Description = "Pan Right", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Zoom In - /// - [JoinName("ZoomIn")] - public JoinDataComplete ZoomIn = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, new JoinMetadata { Description = "Zoom In", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Zoom Out - /// - [JoinName("ZoomOut")] - public JoinDataComplete ZoomOut = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, new JoinMetadata { Description = "Zoom Out", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Is Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, new JoinMetadata { Description = "Is Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Power On - /// - [JoinName("PowerOn")] - public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, new JoinMetadata { Description = "Power On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Power Off - /// - [JoinName("PowerOff")] - public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, new JoinMetadata { Description = "Power Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Number Of Presets - /// - [JoinName("NumberOfPresets")] - public JoinDataComplete NumberOfPresets = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, new JoinMetadata { Description = "Tells Essentials the number of defined presets", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Preset Recall Start - /// - [JoinName("PresetRecallStart")] - public JoinDataComplete PresetRecallStart = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 20 }, new JoinMetadata { Description = "Preset Recall Start", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Preset Label Start - /// - [JoinName("PresetLabelStart")] - public JoinDataComplete PresetLabelStart = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 20 }, new JoinMetadata { Description = "Preset Label Start", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Preset Save Start - /// - [JoinName("PresetSaveStart")] - public JoinDataComplete PresetSaveStart = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 20 }, new JoinMetadata { Description = "Preset Save Start", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Camera Mode Auto - /// - [JoinName("CameraModeAuto")] - public JoinDataComplete CameraModeAuto = new JoinDataComplete(new JoinData { JoinNumber = 51, JoinSpan = 1 }, new JoinMetadata { Description = "Camera Mode Auto", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Camera Mode Manual - /// - [JoinName("CameraModeManual")] - public JoinDataComplete CameraModeManual = new JoinDataComplete(new JoinData { JoinNumber = 52, JoinSpan = 1 }, new JoinMetadata { Description = "Camera Mode Manual", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Camera Mode Off - /// - [JoinName("CameraModeOff")] - public JoinDataComplete CameraModeOff = new JoinDataComplete(new JoinData { JoinNumber = 53, JoinSpan = 1 }, new JoinMetadata { Description = "Camera Mode Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Supports Camera Mode Manual - /// - [JoinName("SupportsCameraModeAuto")] - public JoinDataComplete SupportsCameraModeAuto = new JoinDataComplete(new JoinData { JoinNumber = 55, JoinSpan = 1 }, new JoinMetadata { Description = "Supports Camera Mode Auto", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Supports Camera Mode Off - /// - [JoinName("SupportsCameraModeOff")] - public JoinDataComplete SupportsCameraModeOff = new JoinDataComplete(new JoinData { JoinNumber = 56, JoinSpan = 1 }, new JoinMetadata { Description = "Supports Camera Mode Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Supports Presets - /// - [JoinName("SupportsPresets")] - public JoinDataComplete SupportsPresets = new JoinDataComplete(new JoinData { JoinNumber = 57, JoinSpan = 1 }, new JoinMetadata { Description = "Supports Presets", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public CameraControllerJoinMap(uint joinStart) - : this(joinStart, typeof(CameraControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected CameraControllerJoinMap(uint joinStart, Type type) : base(joinStart, type){} } + + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected CameraControllerJoinMap(uint joinStart, Type type) : base(joinStart, type){} } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/CenOdtOccupancySensorBaseJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/CenOdtOccupancySensorBaseJoinMap.cs index b52c5309..b585916b 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/CenOdtOccupancySensorBaseJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/CenOdtOccupancySensorBaseJoinMap.cs @@ -1,320 +1,196 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class CenOdtOccupancySensorBaseJoinMap : JoinMapBaseAdvanced { + #region Digitals + + [JoinName("Online")] + public JoinDataComplete Online = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ForceOccupied")] + public JoinDataComplete ForceOccupied = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Force Occupied", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ForceVacant")] + public JoinDataComplete ForceVacant = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Force Vacant", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableRawStates")] + public JoinDataComplete EnableRawStates = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Enable Raw States", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RoomOccupiedFeedback")] + public JoinDataComplete RoomOccupiedFeedback = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Room Occupied Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("GraceOccupancyDetectedFeedback")] + public JoinDataComplete GraceOccupancyDetectedFeedback = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Grace Occupancy Detected Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RoomVacantFeedback")] + public JoinDataComplete RoomVacantFeedback = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Room Vacant Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RawOccupancyFeedback")] + public JoinDataComplete RawOccupancyFeedback = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Raw Occupancy Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RawOccupancyPirFeedback")] + public JoinDataComplete RawOccupancyPirFeedback = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Raw Occupancy Pir Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RawOccupancyUsFeedback")] + public JoinDataComplete RawOccupancyUsFeedback = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "Raw Occupancy Us Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IdentityModeOn")] + public JoinDataComplete IdentityMode = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "Enable Identity Mode", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IdentityModeFeedback")] + public JoinDataComplete IdentityModeFeedback = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "Identity Mode Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableLedFlash")] + public JoinDataComplete EnableLedFlash = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Enable Led Flash", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableLedFlash")] + public JoinDataComplete DisableLedFlash = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "Disable Led Flash", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableShortTimeout")] + public JoinDataComplete EnableShortTimeout = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "Enable Short Timeout", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableShortTimeout")] + public JoinDataComplete DisableShortTimeout = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "Disable Short Timeout", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OrWhenVacated")] + public JoinDataComplete OrWhenVacated = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "Or When Vacated", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("AndWhenVacated")] + public JoinDataComplete AndWhenVacated = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, + new JoinMetadata { Description = "AndWhenVacated", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableUsA")] + public JoinDataComplete EnableUsA = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, + new JoinMetadata { Description = "Enable Us A", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableUsA")] + public JoinDataComplete DisableUsA = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, + new JoinMetadata { Description = "Disable Us A", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableUsB")] + public JoinDataComplete EnableUsB = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, + new JoinMetadata { Description = "Enable Us B", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableUsB")] + public JoinDataComplete DisableUsB = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, + new JoinMetadata { Description = "Disable Us B", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnablePir")] + public JoinDataComplete EnablePir = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata { Description = "Enable Pir", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisablePir")] + public JoinDataComplete DisablePir = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata { Description = "Disable Pir", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IncrementUsInOccupiedState")] + public JoinDataComplete IncrementUsInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, + new JoinMetadata { Description = "Increment Us In OccupiedState", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DecrementUsInOccupiedState")] + public JoinDataComplete DecrementUsInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, + new JoinMetadata { Description = "Dencrement Us In Occupied State", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IncrementUsInVacantState")] + public JoinDataComplete IncrementUsInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, + new JoinMetadata { Description = "Increment Us In VacantState", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DecrementUsInVacantState")] + public JoinDataComplete DecrementUsInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, + new JoinMetadata { Description = "Decrement Us In VacantState", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IncrementPirInOccupiedState")] + public JoinDataComplete IncrementPirInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, + new JoinMetadata { Description = "Increment Pir In Occupied State", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DecrementPirInOccupiedState")] + public JoinDataComplete DecrementPirInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, + new JoinMetadata { Description = "Decrement Pir In OccupiedState", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IncrementPirInVacantState")] + public JoinDataComplete IncrementPirInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, + new JoinMetadata { Description = "Increment Pir In Vacant State", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DecrementPirInVacantState")] + public JoinDataComplete DecrementPirInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, + new JoinMetadata { Description = "Decrement Pir In Vacant State", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + #endregion + + #region Analog + + [JoinName("Timeout")] + public JoinDataComplete Timeout = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Timeout", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("TimeoutLocalFeedback")] + public JoinDataComplete TimeoutLocalFeedback = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Timeout Local Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("InternalPhotoSensorValue")] + public JoinDataComplete InternalPhotoSensorValue = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Internal PhotoSensor Value", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("UsSensitivityInOccupiedState")] + public JoinDataComplete UsSensitivityInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Us Sensitivity In Occupied State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("UsSensitivityInVacantState")] + public JoinDataComplete UsSensitivityInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Us Sensitivity In Vacant State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("PirSensitivityInOccupiedState")] + public JoinDataComplete PirSensitivityInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "Pir Sensitivity In Occupied State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("PirSensitivityInVacantState")] + public JoinDataComplete PirSensitivityInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "Pir Sensitivity In Vacant State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + #endregion + + #region Serial + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + #endregion + /// - /// Represents a CenOdtOccupancySensorBaseJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class CenOdtOccupancySensorBaseJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public CenOdtOccupancySensorBaseJoinMap(uint joinStart) + : this(joinStart, typeof(CenOdtOccupancySensorBaseJoinMap)) { - #region Digitals - - /// - /// Online - /// - [JoinName("Online")] - public JoinDataComplete Online = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Force Occupied - /// - [JoinName("ForceOccupied")] - public JoinDataComplete ForceOccupied = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Force Occupied", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Force Vacant - /// - [JoinName("ForceVacant")] - public JoinDataComplete ForceVacant = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Force Vacant", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Enable Raw States - /// - [JoinName("EnableRawStates")] - public JoinDataComplete EnableRawStates = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Enable Raw States", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Disable Raw States - /// - [JoinName("RoomOccupiedFeedback")] - public JoinDataComplete RoomOccupiedFeedback = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Room Occupied Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Grace Occupancy Detected Feedback - /// - [JoinName("GraceOccupancyDetectedFeedback")] - public JoinDataComplete GraceOccupancyDetectedFeedback = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Grace Occupancy Detected Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Room Vacant Feedback - /// - [JoinName("RoomVacantFeedback")] - public JoinDataComplete RoomVacantFeedback = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Room Vacant Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Raw Occupancy Feedback - /// - [JoinName("RawOccupancyFeedback")] - public JoinDataComplete RawOccupancyFeedback = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Raw Occupancy Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Raw Occupancy Pir Feedback - /// - [JoinName("RawOccupancyPirFeedback")] - public JoinDataComplete RawOccupancyPirFeedback = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Raw Occupancy Pir Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Raw Occupancy Us Feedback - /// - [JoinName("RawOccupancyUsFeedback")] - public JoinDataComplete RawOccupancyUsFeedback = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "Raw Occupancy Us Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Identity Mode On - /// - [JoinName("IdentityModeOn")] - public JoinDataComplete IdentityMode = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "Enable Identity Mode", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Identity Mode Off - /// - [JoinName("IdentityModeFeedback")] - public JoinDataComplete IdentityModeFeedback = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "Identity Mode Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Enable Led Flash - /// - [JoinName("EnableLedFlash")] - public JoinDataComplete EnableLedFlash = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Enable Led Flash", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Disable Led Flash - /// - [JoinName("DisableLedFlash")] - public JoinDataComplete DisableLedFlash = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "Disable Led Flash", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Enable Short Timeout - /// - [JoinName("EnableShortTimeout")] - public JoinDataComplete EnableShortTimeout = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "Enable Short Timeout", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Disable Short Timeout - /// - [JoinName("DisableShortTimeout")] - public JoinDataComplete DisableShortTimeout = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "Disable Short Timeout", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Or When Vacated - /// - [JoinName("OrWhenVacated")] - public JoinDataComplete OrWhenVacated = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "Or When Vacated", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// And When Vacated - /// - [JoinName("AndWhenVacated")] - public JoinDataComplete AndWhenVacated = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, - new JoinMetadata { Description = "AndWhenVacated", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Enable Us A - /// - [JoinName("EnableUsA")] - public JoinDataComplete EnableUsA = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, - new JoinMetadata { Description = "Enable Us A", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Disable Us A - /// - [JoinName("DisableUsA")] - public JoinDataComplete DisableUsA = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, - new JoinMetadata { Description = "Disable Us A", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Enable Us B - /// - [JoinName("EnableUsB")] - public JoinDataComplete EnableUsB = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, - new JoinMetadata { Description = "Enable Us B", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Disable Us B - /// - [JoinName("DisableUsB")] - public JoinDataComplete DisableUsB = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, - new JoinMetadata { Description = "Disable Us B", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Enable Pir - /// - [JoinName("EnablePir")] - public JoinDataComplete EnablePir = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, - new JoinMetadata { Description = "Enable Pir", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Disable Pir - /// - [JoinName("DisablePir")] - public JoinDataComplete DisablePir = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, - new JoinMetadata { Description = "Disable Pir", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Increment Us In Occupied State - /// - [JoinName("IncrementUsInOccupiedState")] - public JoinDataComplete IncrementUsInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, - new JoinMetadata { Description = "Increment Us In OccupiedState", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Decrement Us In Occupied State - /// - [JoinName("DecrementUsInOccupiedState")] - public JoinDataComplete DecrementUsInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, - new JoinMetadata { Description = "Dencrement Us In Occupied State", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Increment Us In Vacant State - /// - [JoinName("IncrementUsInVacantState")] - public JoinDataComplete IncrementUsInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, - new JoinMetadata { Description = "Increment Us In VacantState", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Decrement Us In Vacant State - /// - [JoinName("DecrementUsInVacantState")] - public JoinDataComplete DecrementUsInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, - new JoinMetadata { Description = "Decrement Us In VacantState", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Increment Pir In Occupied State - /// - [JoinName("IncrementPirInOccupiedState")] - public JoinDataComplete IncrementPirInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, - new JoinMetadata { Description = "Increment Pir In Occupied State", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Decrement Pir In Occupied State - /// - [JoinName("DecrementPirInOccupiedState")] - public JoinDataComplete DecrementPirInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, - new JoinMetadata { Description = "Decrement Pir In OccupiedState", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Increment Pir In Vacant State - /// - [JoinName("IncrementPirInVacantState")] - public JoinDataComplete IncrementPirInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, - new JoinMetadata { Description = "Increment Pir In Vacant State", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Decrement Pir In Vacant State - /// - [JoinName("DecrementPirInVacantState")] - public JoinDataComplete DecrementPirInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, - new JoinMetadata { Description = "Decrement Pir In Vacant State", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - #endregion - - #region Analog - /// - /// Timeout - /// - [JoinName("Timeout")] - public JoinDataComplete Timeout = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Timeout", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Timeout Local Feedback - /// - [JoinName("TimeoutLocalFeedback")] - public JoinDataComplete TimeoutLocalFeedback = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Timeout Local Feedback", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Internal PhotoSensor Value - /// - [JoinName("InternalPhotoSensorValue")] - public JoinDataComplete InternalPhotoSensorValue = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Internal PhotoSensor Value", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// External PhotoSensor Value - /// - [JoinName("UsSensitivityInOccupiedState")] - public JoinDataComplete UsSensitivityInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Us Sensitivity In Occupied State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Us Sensitivity In Vacant State - /// - [JoinName("UsSensitivityInVacantState")] - public JoinDataComplete UsSensitivityInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Us Sensitivity In Vacant State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Pir Sensitivity In Occupied State - /// - [JoinName("PirSensitivityInOccupiedState")] - public JoinDataComplete PirSensitivityInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "Pir Sensitivity In Occupied State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Pir Sensitivity In Vacant State - /// - [JoinName("PirSensitivityInVacantState")] - public JoinDataComplete PirSensitivityInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "Pir Sensitivity In Vacant State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - #endregion - - #region Serial - - /// - /// Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - #endregion - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public CenOdtOccupancySensorBaseJoinMap(uint joinStart) - : this(joinStart, typeof(CenOdtOccupancySensorBaseJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected CenOdtOccupancySensorBaseJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected CenOdtOccupancySensorBaseJoinMap(uint joinStart, Type type) : base(joinStart, type) + { + } } diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DisplayControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DisplayControllerJoinMap.cs index 3741f951..2ab95b8e 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DisplayControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DisplayControllerJoinMap.cs @@ -1,133 +1,84 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class DisplayControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("PowerOff")] + public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Power Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PowerOn")] + public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Power On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IsTwoWayDisplay")] + public JoinDataComplete IsTwoWayDisplay = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Is Two Way Display", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VolumeUp")] + public JoinDataComplete VolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Volume Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VolumeLevel")] + public JoinDataComplete VolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Volume Level", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("VolumeDown")] + public JoinDataComplete VolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Volume Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VolumeMute")] + public JoinDataComplete VolumeMute = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "Volume Mute", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VolumeMuteOn")] + public JoinDataComplete VolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "Volume Mute On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VolumeMuteOff")] + public JoinDataComplete VolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, + new JoinMetadata { Description = "Volume Mute Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("InputSelectOffset")] + public JoinDataComplete InputSelectOffset = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 10 }, + new JoinMetadata { Description = "Input Select", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("InputNamesOffset")] + public JoinDataComplete InputNamesOffset = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 10 }, + new JoinMetadata { Description = "Input Names Offset", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("InputSelect")] + public JoinDataComplete InputSelect = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Input Select", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("ButtonVisibilityOffset")] + public JoinDataComplete ButtonVisibilityOffset = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 10 }, + new JoinMetadata { Description = "Button Visibility Offset", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.DigitalSerial }); + + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1 }, + new JoinMetadata { Description = "Is Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + /// - /// Represents a DisplayControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class DisplayControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public DisplayControllerJoinMap(uint joinStart) + : this(joinStart, typeof(DisplayControllerJoinMap)) { - /// - /// Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Power Off - /// - [JoinName("PowerOff")] - public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Power Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Power On - /// - [JoinName("PowerOn")] - public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Power On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Is Two Way Display - /// - [JoinName("IsTwoWayDisplay")] - public JoinDataComplete IsTwoWayDisplay = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Is Two Way Display", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Volume Up - /// - [JoinName("VolumeUp")] - public JoinDataComplete VolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Volume Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Volume Level - /// - [JoinName("VolumeLevel")] - public JoinDataComplete VolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Volume Level", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Volume Down - /// - [JoinName("VolumeDown")] - public JoinDataComplete VolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Volume Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Volume Mute - /// - [JoinName("VolumeMute")] - public JoinDataComplete VolumeMute = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "Volume Mute", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Volume Mute On - /// - [JoinName("VolumeMuteOn")] - public JoinDataComplete VolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "Volume Mute On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Volume Mute Off - /// - [JoinName("VolumeMuteOff")] - public JoinDataComplete VolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, - new JoinMetadata { Description = "Volume Mute Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Input Select Offset - /// - [JoinName("InputSelectOffset")] - public JoinDataComplete InputSelectOffset = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 10 }, - new JoinMetadata { Description = "Input Select", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Input Names Offset - /// - [JoinName("InputNamesOffset")] - public JoinDataComplete InputNamesOffset = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 10 }, - new JoinMetadata { Description = "Input Names Offset", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Input Select - /// - [JoinName("InputSelect")] - public JoinDataComplete InputSelect = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Input Select", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Button Visibility Offset - /// - [JoinName("ButtonVisibilityOffset")] - public JoinDataComplete ButtonVisibilityOffset = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 10 }, - new JoinMetadata { Description = "Button Visibility Offset", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.DigitalSerial }); - - /// - /// Is Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1 }, - new JoinMetadata { Description = "Is Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + } - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public DisplayControllerJoinMap(uint joinStart) - : this(joinStart, typeof(DisplayControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected DisplayControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected DisplayControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmBladeChassisControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmBladeChassisControllerJoinMap.cs index c6488bba..3edd1222 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmBladeChassisControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmBladeChassisControllerJoinMap.cs @@ -1,113 +1,73 @@ using System; -namespace PepperDash.Essentials.Core.Bridges { +namespace PepperDash.Essentials.Core.Bridges; +public class DmBladeChassisControllerJoinMap : JoinMapBaseAdvanced { + + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "DM Blade Chassis Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoSyncStatus")] + public JoinDataComplete VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Input Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("InputEndpointOnline")] + public JoinDataComplete InputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Input Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutputEndpointOnline")] + public JoinDataComplete OutputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Output Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("TxAdvancedIsPresent")] + public JoinDataComplete TxAdvancedIsPresent = new JoinDataComplete(new JoinData { JoinNumber = 1001, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Tx Advanced Is Present", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutputVideo")] + public JoinDataComplete OutputVideo = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Output Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("HdcpSupportState")] + public JoinDataComplete HdcpSupportState = new JoinDataComplete(new JoinData { JoinNumber = 1001, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Input HDCP Support State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("HdcpSupportCapability")] + public JoinDataComplete HdcpSupportCapability = new JoinDataComplete(new JoinData { JoinNumber = 1201, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Input HDCP Support Capability", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("InputNames")] + public JoinDataComplete InputNames = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("OutputNames")] + public JoinDataComplete OutputNames = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Output Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("OutputCurrentVideoInputNames")] + public JoinDataComplete OutputCurrentVideoInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2001, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Video Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("InputCurrentResolution")] + public JoinDataComplete InputCurrentResolution = new JoinDataComplete(new JoinData { JoinNumber = 2401, JoinSpan = 128 }, + new JoinMetadata { Description = "DM Blade Chassis Input Current Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + /// - /// Represents a DmBladeChassisControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class DmBladeChassisControllerJoinMap : JoinMapBaseAdvanced { - - /// - /// DM Blade Chassis Online status - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "DM Blade Chassis Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Blade Input Video Sync - /// - [JoinName("VideoSyncStatus")] - public JoinDataComplete VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Input Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Blade Chassis Input Endpoint Online - /// - [JoinName("InputEndpointOnline")] - public JoinDataComplete InputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Input Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Blade Chassis Output Endpoint Online - /// - [JoinName("OutputEndpointOnline")] - public JoinDataComplete OutputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Output Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Blade Chassis Tx Advanced Is Present - /// - [JoinName("TxAdvancedIsPresent")] - public JoinDataComplete TxAdvancedIsPresent = new JoinDataComplete(new JoinData { JoinNumber = 1001, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Tx Advanced Is Present", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Blade Chassis Rx Advanced Is Present - /// - [JoinName("OutputVideo")] - public JoinDataComplete OutputVideo = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Output Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Blade Chassis Input HDCP Support State - /// - [JoinName("HdcpSupportState")] - public JoinDataComplete HdcpSupportState = new JoinDataComplete(new JoinData { JoinNumber = 1001, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Input HDCP Support State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Blade Chassis Input HDCP Support Capability - /// - [JoinName("HdcpSupportCapability")] - public JoinDataComplete HdcpSupportCapability = new JoinDataComplete(new JoinData { JoinNumber = 1201, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Input HDCP Support Capability", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Blade Chassis Input Names - /// - [JoinName("InputNames")] - public JoinDataComplete InputNames = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Blade Chassis Output Names - /// - [JoinName("OutputNames")] - public JoinDataComplete OutputNames = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Output Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Blade Chassis Video Output Currently Routed Video Input Name - /// - [JoinName("OutputCurrentVideoInputNames")] - public JoinDataComplete OutputCurrentVideoInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2001, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Video Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Blade Chassis Input Current Resolution - /// - [JoinName("InputCurrentResolution")] - public JoinDataComplete InputCurrentResolution = new JoinDataComplete(new JoinData { JoinNumber = 2401, JoinSpan = 128 }, - new JoinMetadata { Description = "DM Blade Chassis Input Current Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public DmBladeChassisControllerJoinMap(uint joinStart) - : this(joinStart, typeof(DmBladeChassisControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected DmBladeChassisControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } - + /// Join this join map will start at + public DmBladeChassisControllerJoinMap(uint joinStart) + : this(joinStart, typeof(DmBladeChassisControllerJoinMap)) + { } + + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected DmBladeChassisControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) + { + } + } diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmChassisControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmChassisControllerJoinMap.cs index 9d2217cc..db3701b3 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmChassisControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmChassisControllerJoinMap.cs @@ -1,259 +1,169 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class DmChassisControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("EnableAudioBreakaway")] + public JoinDataComplete EnableAudioBreakaway = new JoinDataComplete( + new JoinData {JoinNumber = 4, JoinSpan = 1}, + new JoinMetadata + { + Description = "DM Chassis enable audio breakaway routing", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("EnableUsbBreakaway")] + public JoinDataComplete EnableUsbBreakaway = new JoinDataComplete( + new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata + { + Description = "DM Chassis enable USB breakaway routing", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "DM Chassis Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("SystemId")] + public JoinDataComplete SystemId = new JoinDataComplete(new JoinData { JoinNumber = 10, JoinSpan = 1 }, + new JoinMetadata { Description = "DM Chassis SystemId Get/Set/Trigger/", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.DigitalAnalog }); + + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "DM Chassis Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoSyncStatus")] + public JoinDataComplete VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Input Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("InputEndpointOnline")] + public JoinDataComplete InputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutputEndpointOnline")] + public JoinDataComplete OutputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("TxAdvancedIsPresent")] + public JoinDataComplete TxAdvancedIsPresent = new JoinDataComplete(new JoinData { JoinNumber = 1001, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Tx Advanced Is Present", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutputDisabledByHdcp")] + public JoinDataComplete OutputDisabledByHdcp = new JoinDataComplete(new JoinData { JoinNumber = 1201, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Disabled by HDCP", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutputVideo")] + public JoinDataComplete OutputVideo = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Video Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("OutputAudio")] + public JoinDataComplete OutputAudio = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Audio Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("OutputUsb")] + public JoinDataComplete OutputUsb = new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output USB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("InputUsb")] + public JoinDataComplete InputUsb = new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input Usb Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("HdcpSupportState")] + public JoinDataComplete HdcpSupportState = new JoinDataComplete(new JoinData { JoinNumber = 1001, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input HDCP Support State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("HdcpSupportCapability")] + public JoinDataComplete HdcpSupportCapability = new JoinDataComplete(new JoinData { JoinNumber = 1201, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input HDCP Support Capability", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("InputStreamCardState")] + public JoinDataComplete InputStreamCardState = new JoinDataComplete(new JoinData { JoinNumber = 1501, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Stream Input Start (1), Stop (2), Pause (3) with Feedback", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("OutputStreamCardState")] + public JoinDataComplete OutputStreamCardState = new JoinDataComplete(new JoinData { JoinNumber = 1601, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Stream Output Start (1), Stop (2), Pause (3) with Feedback", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("NoRouteName")] + public JoinDataComplete NoRouteName = new JoinDataComplete(new JoinData { JoinNumber = 100, JoinSpan = 1 }, + new JoinMetadata { Description = "DM Chassis Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("InputNames")] + public JoinDataComplete InputNames = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("OutputNames")] + public JoinDataComplete OutputNames = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("InputVideoNames")] public JoinDataComplete InputVideoNames = + new JoinDataComplete(new JoinData {JoinNumber = 501, JoinSpan = 200}, + new JoinMetadata + { + Description = "DM Chassis Video Input Names", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("InputAudioNames")] + public JoinDataComplete InputAudioNames = + new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 200 }, + new JoinMetadata + { + Description = "DM Chassis Audio Input Names", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + [JoinName("OutputVideoNames")] + public JoinDataComplete OutputVideoNames = + new JoinDataComplete(new JoinData { JoinNumber = 901, JoinSpan = 200 }, + new JoinMetadata + { + Description = "DM Chassis Video Output Names", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + [JoinName("OutputAudioNames")] + public JoinDataComplete OutputAudioNames = + new JoinDataComplete(new JoinData { JoinNumber = 1101, JoinSpan = 200 }, + new JoinMetadata + { + Description = "DM Chassis Audio Output Names", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("OutputCurrentVideoInputNames")] + public JoinDataComplete OutputCurrentVideoInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2001, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Video Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("OutputCurrentAudioInputNames")] + public JoinDataComplete OutputCurrentAudioInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2201, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Audio Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("InputCurrentResolution")] + public JoinDataComplete InputCurrentResolution = new JoinDataComplete(new JoinData { JoinNumber = 2401, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input Current Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + /// - /// Represents a DmChassisControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class DmChassisControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public DmChassisControllerJoinMap(uint joinStart) + : this(joinStart, typeof(DmChassisControllerJoinMap)) { - /// - /// DM Chassis enable audio breakaway routing - /// - [JoinName("EnableAudioBreakaway")] - public JoinDataComplete EnableAudioBreakaway = new JoinDataComplete( - new JoinData {JoinNumber = 4, JoinSpan = 1}, - new JoinMetadata - { - Description = "DM Chassis enable audio breakaway routing", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); - - /// - /// DM Chassis enable USB breakaway routing - /// - [JoinName("EnableUsbBreakaway")] - public JoinDataComplete EnableUsbBreakaway = new JoinDataComplete( - new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata - { - Description = "DM Chassis enable USB breakaway routing", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); - - /// - /// DM Chassis Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "DM Chassis Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis SystemId Get/Set/Trigger - /// - [JoinName("SystemId")] - public JoinDataComplete SystemId = new JoinDataComplete(new JoinData { JoinNumber = 10, JoinSpan = 1 }, - new JoinMetadata { Description = "DM Chassis SystemId Get/Set/Trigger/", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.DigitalAnalog }); - - /// - /// DM Chassis Online status - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "DM Chassis Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Input Video Sync - /// - [JoinName("VideoSyncStatus")] - public JoinDataComplete VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Input Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Chassis Input Endpoint Online - /// - [JoinName("InputEndpointOnline")] - public JoinDataComplete InputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Chassis Output Endpoint Online - /// - [JoinName("OutputEndpointOnline")] - public JoinDataComplete OutputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Chassis Tx Advanced Is Present - /// - [JoinName("TxAdvancedIsPresent")] - public JoinDataComplete TxAdvancedIsPresent = new JoinDataComplete(new JoinData { JoinNumber = 1001, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Tx Advanced Is Present", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Chassis Rx Advanced Is Present - /// - [JoinName("OutputDisabledByHdcp")] - public JoinDataComplete OutputDisabledByHdcp = new JoinDataComplete(new JoinData { JoinNumber = 1201, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Disabled by HDCP", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Chassis Output Set / Get - /// - [JoinName("OutputVideo")] - public JoinDataComplete OutputVideo = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Video Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Output Audio Set / Get - /// - [JoinName("OutputAudio")] - public JoinDataComplete OutputAudio = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Audio Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Input Set / Get - /// - [JoinName("OutputUsb")] - public JoinDataComplete OutputUsb = new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output USB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Input Set / Get - /// - [JoinName("InputUsb")] - public JoinDataComplete InputUsb = new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input Usb Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Input HDCP Support State - /// - [JoinName("HdcpSupportState")] - public JoinDataComplete HdcpSupportState = new JoinDataComplete(new JoinData { JoinNumber = 1001, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input HDCP Support State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Input HDCP Support Capability - /// - [JoinName("HdcpSupportCapability")] - public JoinDataComplete HdcpSupportCapability = new JoinDataComplete(new JoinData { JoinNumber = 1201, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input HDCP Support Capability", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Stream Input Start (1), Stop (2), Pause (3) with Feedback - /// - [JoinName("InputStreamCardState")] - public JoinDataComplete InputStreamCardState = new JoinDataComplete(new JoinData { JoinNumber = 1501, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Stream Input Start (1), Stop (2), Pause (3) with Feedback", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Stream Output Start (1), Stop (2), Pause (3) with Feedback - /// - [JoinName("OutputStreamCardState")] - public JoinDataComplete OutputStreamCardState = new JoinDataComplete(new JoinData { JoinNumber = 1601, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Stream Output Start (1), Stop (2), Pause (3) with Feedback", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis No Route Name - /// - [JoinName("NoRouteName")] - public JoinDataComplete NoRouteName = new JoinDataComplete(new JoinData { JoinNumber = 100, JoinSpan = 1 }, - new JoinMetadata { Description = "DM Chassis Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Input Names - /// - [JoinName("InputNames")] - public JoinDataComplete InputNames = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Output Names - /// - [JoinName("OutputNames")] - public JoinDataComplete OutputNames = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Video Input Names - /// - [JoinName("InputVideoNames")] public JoinDataComplete InputVideoNames = - new JoinDataComplete(new JoinData {JoinNumber = 501, JoinSpan = 200}, - new JoinMetadata - { - Description = "DM Chassis Video Input Names", - JoinCapabilities = eJoinCapabilities.ToFromSIMPL, - JoinType = eJoinType.Serial - }); - - /// - /// DM Chassis Audio Input Names - /// - [JoinName("InputAudioNames")] - public JoinDataComplete InputAudioNames = - new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 200 }, - new JoinMetadata - { - Description = "DM Chassis Audio Input Names", - JoinCapabilities = eJoinCapabilities.ToFromSIMPL, - JoinType = eJoinType.Serial - }); - - /// - /// DM Chassis Video Output Names - /// - [JoinName("OutputVideoNames")] - public JoinDataComplete OutputVideoNames = - new JoinDataComplete(new JoinData { JoinNumber = 901, JoinSpan = 200 }, - new JoinMetadata - { - Description = "DM Chassis Video Output Names", - JoinCapabilities = eJoinCapabilities.ToFromSIMPL, - JoinType = eJoinType.Serial - }); - - /// - /// DM Chassis Audio Output Names - /// - [JoinName("OutputAudioNames")] - public JoinDataComplete OutputAudioNames = - new JoinDataComplete(new JoinData { JoinNumber = 1101, JoinSpan = 200 }, - new JoinMetadata - { - Description = "DM Chassis Audio Output Names", - JoinCapabilities = eJoinCapabilities.ToFromSIMPL, - JoinType = eJoinType.Serial - }); - - /// - /// DM Chassis Video Output Currently Routed Video Input Name - /// - [JoinName("OutputCurrentVideoInputNames")] - public JoinDataComplete OutputCurrentVideoInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2001, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Video Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Audio Output Currently Routed Audio Input Name - /// - [JoinName("OutputCurrentAudioInputNames")] - public JoinDataComplete OutputCurrentAudioInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2201, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Audio Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Input Current Resolution - /// - [JoinName("InputCurrentResolution")] - public JoinDataComplete InputCurrentResolution = new JoinDataComplete(new JoinData { JoinNumber = 2401, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input Current Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + } - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public DmChassisControllerJoinMap(uint joinStart) - : this(joinStart, typeof(DmChassisControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected DmChassisControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected DmChassisControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) + { } } diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmRmcControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmRmcControllerJoinMap.cs index 6f0df102..4ddb9ff4 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmRmcControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmRmcControllerJoinMap.cs @@ -1,143 +1,91 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class DmRmcControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoMuteOn")] + public JoinDataComplete VideoMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC Mute Video", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoMuteOff")] + public JoinDataComplete VideoMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC UnMute Video", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoMuteToggle")] + public JoinDataComplete VideoMuteToggle = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC Mute Video Toggle", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("CurrentOutputResolution")] + public JoinDataComplete CurrentOutputResolution = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC Current Output Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("EdidManufacturer")] + public JoinDataComplete EdidManufacturer = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC EDID Manufacturer", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("EdidName")] + public JoinDataComplete EdidName = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC EDID Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("EdidPrefferedTiming")] + public JoinDataComplete EdidPrefferedTiming = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC EDID Preferred Timing", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("EdidSerialNumber")] + public JoinDataComplete EdidSerialNumber = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC EDID Serial Number", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("AudioVideoSource")] + public JoinDataComplete AudioVideoSource = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC Audio Video Source Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("HdcpSupportCapability")] + public JoinDataComplete HdcpSupportCapability = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC HDCP Support Capability", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Port1HdcpState")] + public JoinDataComplete Port1HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC Port 1 (DM) HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Port2HdcpState")] + public JoinDataComplete Port2HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Port 2 (HDMI) HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("HdmiInputSync")] + public JoinDataComplete HdmiInputSync = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "DM RMC HDMI Input Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HdcpInputPortCount")] + public JoinDataComplete HdcpInputPortCount = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Number of Input Ports that support HDCP", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + + /// - /// Represents a DmRmcControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class DmRmcControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public DmRmcControllerJoinMap(uint joinStart) + : this(joinStart, typeof(DmRmcControllerJoinMap)) { - /// - /// DM RMC Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM RMC Mute Video - /// - [JoinName("VideoMuteOn")] - public JoinDataComplete VideoMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC Mute Video", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM RMC UnMute Video - /// - [JoinName("VideoMuteOff")] - public JoinDataComplete VideoMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC UnMute Video", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM RMC Mute Video Toggle - /// - [JoinName("VideoMuteToggle")] - public JoinDataComplete VideoMuteToggle = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC Mute Video Toggle", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM RMC Current Output Resolution - /// - [JoinName("CurrentOutputResolution")] - public JoinDataComplete CurrentOutputResolution = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC Current Output Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM RMC EDID Manufacturer - /// - [JoinName("EdidManufacturer")] - public JoinDataComplete EdidManufacturer = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC EDID Manufacturer", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM RMC EDID Name - /// - [JoinName("EdidName")] - public JoinDataComplete EdidName = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC EDID Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM RMC EDID Preferred Timing - /// - [JoinName("EdidPrefferedTiming")] - public JoinDataComplete EdidPrefferedTiming = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC EDID Preferred Timing", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM RMC EDID Serial Number - /// - [JoinName("EdidSerialNumber")] - public JoinDataComplete EdidSerialNumber = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC EDID Serial Number", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM RMC Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM RMC Audio Video Source Set / Get - /// - [JoinName("AudioVideoSource")] - public JoinDataComplete AudioVideoSource = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC Audio Video Source Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM RMC HDCP Support Capability - /// - [JoinName("HdcpSupportCapability")] - public JoinDataComplete HdcpSupportCapability = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC HDCP Support Capability", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM RMC Port 1 (DM) HDCP State Set / Get - /// - [JoinName("Port1HdcpState")] - public JoinDataComplete Port1HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC Port 1 (DM) HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM RMC Port 2 (HDMI) HDCP State Set / Get - /// - [JoinName("Port2HdcpState")] - public JoinDataComplete Port2HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC Port 2 (HDMI) HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM RMC HDMI Input Sync - /// - [JoinName("HdmiInputSync")] - public JoinDataComplete HdmiInputSync = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "DM RMC HDMI Input Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM RMC Number of Input Ports that support HDCP - /// - [JoinName("HdcpInputPortCount")] - public JoinDataComplete HdcpInputPortCount = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Number of Input Ports that support HDCP", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + } - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public DmRmcControllerJoinMap(uint joinStart) - : this(joinStart, typeof(DmRmcControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected DmRmcControllerJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected DmRmcControllerJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmTxControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmTxControllerJoinMap.cs index ab1299d1..8fb99f33 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmTxControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmTxControllerJoinMap.cs @@ -1,150 +1,95 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class DmTxControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoSyncStatus")] + public JoinDataComplete VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("FreeRunEnabled")] + public JoinDataComplete FreeRunEnabled = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Enable Free Run Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Input1VideoSyncStatus")] + public JoinDataComplete Input1VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Input 1 Video Sync Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Input2VideoSyncStatus")] + public JoinDataComplete Input2VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Input 2 Video Sync Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Input3VideoSyncStatus")] + public JoinDataComplete Input3VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Input 3 Video Sync Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("CurrentInputResolution")] + public JoinDataComplete CurrentInputResolution = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Current Input Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("VideoInput")] + public JoinDataComplete VideoInput = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Video Input Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("AudioInput")] + public JoinDataComplete AudioInput = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Audio Input Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("HdcpSupportCapability")] + public JoinDataComplete HdcpSupportCapability = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX HDCP Support Capability", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Port1HdcpState")] + public JoinDataComplete Port1HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Port 1 HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Port2HdcpState")] + public JoinDataComplete Port2HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Port 2 HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("VgaBrightness")] + public JoinDataComplete VgaBrightness = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX VGA Brightness", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("VgaContrast")] + public JoinDataComplete VgaContrast = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Online", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Port3HdcpState")] + public JoinDataComplete Port3HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "DM TX Port 3 HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("HdcpInputPortCount")] + public JoinDataComplete HdcpInputPortCount = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, + new JoinMetadata { Description = "Number of Input Ports that support HDCP", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + + /// - /// Represents a DmTxControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class DmTxControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public DmTxControllerJoinMap(uint joinStart) + : this(joinStart, typeof(DmTxControllerJoinMap)) { - /// - /// DM TX Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM TX Video Sync - /// - [JoinName("VideoSyncStatus")] - public JoinDataComplete VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM TX Enable Free Run Set / Get - /// - [JoinName("FreeRunEnabled")] - public JoinDataComplete FreeRunEnabled = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Enable Free Run Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Input 1 Video Sync Status - /// - [JoinName("Input1VideoSyncStatus")] - public JoinDataComplete Input1VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Input 1 Video Sync Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Input 2 Video Sync Status - /// - [JoinName("Input2VideoSyncStatus")] - public JoinDataComplete Input2VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Input 2 Video Sync Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Input 3 Video Sync Status - /// - [JoinName("Input3VideoSyncStatus")] - public JoinDataComplete Input3VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Input 3 Video Sync Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM TX Current Input Resolution - /// - [JoinName("CurrentInputResolution")] - public JoinDataComplete CurrentInputResolution = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Current Input Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM TX Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM TX Video Input Set / Get - /// - [JoinName("VideoInput")] - public JoinDataComplete VideoInput = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Video Input Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM TX Audio Input Set / Get - /// - [JoinName("AudioInput")] - public JoinDataComplete AudioInput = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Audio Input Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM TX HDCP Support Capability - /// - [JoinName("HdcpSupportCapability")] - public JoinDataComplete HdcpSupportCapability = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX HDCP Support Capability", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM TX Port 1 HDCP State Set / Get - /// - [JoinName("Port1HdcpState")] - public JoinDataComplete Port1HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Port 1 HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM TX Port 2 HDCP State Set / Get - /// - [JoinName("Port2HdcpState")] - public JoinDataComplete Port2HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Port 2 HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM TX VGA Brightness - /// - [JoinName("VgaBrightness")] - public JoinDataComplete VgaBrightness = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX VGA Brightness", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM TX VGA Contrast - /// - [JoinName("VgaContrast")] - public JoinDataComplete VgaContrast = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Online", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM TX Port 3 HDCP State Set / Get - /// - [JoinName("Port3HdcpState")] - public JoinDataComplete Port3HdcpState = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "DM TX Port 3 HDCP State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM TX Number of Input Ports that support HDCP - /// - [JoinName("HdcpInputPortCount")] - public JoinDataComplete HdcpInputPortCount = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, - new JoinMetadata { Description = "Number of Input Ports that support HDCP", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + } - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public DmTxControllerJoinMap(uint joinStart) - : this(joinStart, typeof(DmTxControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected DmTxControllerJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected DmTxControllerJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsAudioOutputControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsAudioOutputControllerJoinMap.cs index ca1d60b4..9ad04691 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsAudioOutputControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsAudioOutputControllerJoinMap.cs @@ -1,288 +1,173 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class DmpsAudioOutputControllerJoinMap : JoinMapBaseAdvanced { + + [JoinName("MasterVolumeLevel")] + public JoinDataComplete MasterVolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Master Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("MasterVolumeLevelScaled")] + public JoinDataComplete MasterVolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Master Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("MixerPresetRecall")] + public JoinDataComplete MixerPresetRecall = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Mixer Preset Recall Set", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("MixerEqPresetRecall")] + public JoinDataComplete MixerEqPresetRecall = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Mixer Eq Preset Recall Set", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("MasterVolumeMuteOn")] + public JoinDataComplete MasterVolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Master Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MasterVolumeMuteOff")] + public JoinDataComplete MasterVolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Master Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MasterVolumeUp")] + public JoinDataComplete MasterVolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Master Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MasterVolumeDown")] + public JoinDataComplete MasterVolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Master Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MasterVolumeLevelScaledSend")] + public JoinDataComplete MasterVolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Master Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SourceVolumeLevel")] + public JoinDataComplete SourceVolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Source Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("SourceVolumeLevelScaled")] + public JoinDataComplete SourceVolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "Source Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("SourceVolumeMuteOn")] + public JoinDataComplete SourceVolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Source Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SourceVolumeMuteOff")] + public JoinDataComplete SourceVolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "Source Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SourceVolumeUp")] + public JoinDataComplete SourceVolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "Source Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SourceVolumeDown")] + public JoinDataComplete SourceVolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "Source Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SourceVolumeLevelScaledSend")] + public JoinDataComplete SourceVolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "Source Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec1VolumeLevel")] + public JoinDataComplete Codec1VolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec1 Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Codec1VolumeLevelScaled")] + public JoinDataComplete Codec1VolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec1 Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Codec1VolumeMuteOn")] + public JoinDataComplete Codec1VolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec1 Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec1VolumeMuteOff")] + public JoinDataComplete Codec1VolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec1 Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec1VolumeUp")] + public JoinDataComplete Codec1VolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec1 Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec1VolumeDown")] + public JoinDataComplete Codec1VolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec1 Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec1VolumeLevelScaledSend")] + public JoinDataComplete Codec1VolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec1 Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec2VolumeLevel")] + public JoinDataComplete Codec2VolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec2 Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Codec2VolumeLevelScaled")] + public JoinDataComplete Codec2VolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec2 Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Codec2VolumeMuteOn")] + public JoinDataComplete Codec2VolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec2 Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec2VolumeMuteOff")] + public JoinDataComplete Codec2VolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec2 Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec2VolumeUp")] + public JoinDataComplete Codec2VolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 33, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec2 Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec2VolumeDown")] + public JoinDataComplete Codec2VolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 34, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec2 Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Codec2VolumeLevelScaledSend")] + public JoinDataComplete Codec2VolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 35, JoinSpan = 1 }, + new JoinMetadata { Description = "Codec2 Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MicsMasterVolumeLevel")] + public JoinDataComplete MicsMasterVolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, + new JoinMetadata { Description = "MicsMaster Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("MicsMasterVolumeLevelScaled")] + public JoinDataComplete MicsMasterVolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, + new JoinMetadata { Description = "MicsMaster Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("MicsMasterVolumeMuteOn")] + public JoinDataComplete MicsMasterVolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, + new JoinMetadata { Description = "MicsMaster Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MicsMasterVolumeMuteOff")] + public JoinDataComplete MicsMasterVolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, + new JoinMetadata { Description = "MicsMaster Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MicsMasterVolumeUp")] + public JoinDataComplete MicsMasterVolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, + new JoinMetadata { Description = "MicsMaster Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MicsMasterVolumeDown")] + public JoinDataComplete MicsMasterVolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 44, JoinSpan = 1 }, + new JoinMetadata { Description = "MicsMaster Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MicsMasterVolumeLevelScaledSend")] + public JoinDataComplete MicsMasterVolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 45, JoinSpan = 1 }, + new JoinMetadata { Description = "Mics Master Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + /// - /// Represents a DmpsAudioOutputControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class DmpsAudioOutputControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public DmpsAudioOutputControllerJoinMap(uint joinStart) + : this(joinStart, typeof(DmpsAudioOutputControllerJoinMap)) { + } - /// - /// Master Volume Level Signed dB Set / Get - /// - [JoinName("MasterVolumeLevel")] - public JoinDataComplete MasterVolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Master Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Master Volume 16bit Scaled Set / Get - /// - [JoinName("MasterVolumeLevelScaled")] - public JoinDataComplete MasterVolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Master Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Mixer Preset Recall Set - /// - [JoinName("MixerPresetRecall")] - public JoinDataComplete MixerPresetRecall = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Mixer Preset Recall Set", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Mixer Eq Preset Recall Set - /// - [JoinName("MixerEqPresetRecall")] - public JoinDataComplete MixerEqPresetRecall = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Mixer Eq Preset Recall Set", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Master Volume Mute On Set / Get - /// - [JoinName("MasterVolumeMuteOn")] - public JoinDataComplete MasterVolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Master Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Master Volume Mute Off Set / Get - /// - [JoinName("MasterVolumeMuteOff")] - public JoinDataComplete MasterVolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Master Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Master Volume Level Up - /// - [JoinName("MasterVolumeUp")] - public JoinDataComplete MasterVolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Master Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Master Volume Level Down - /// - [JoinName("MasterVolumeDown")] - public JoinDataComplete MasterVolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Master Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Master Volume Scaled Send Enable/Disable - /// - [JoinName("MasterVolumeLevelScaledSend")] - public JoinDataComplete MasterVolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Master Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Source Volume Signed dB Set / Get - /// - [JoinName("SourceVolumeLevel")] - public JoinDataComplete SourceVolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Source Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Source Volume 16bit Scaled Set / Get - /// - [JoinName("SourceVolumeLevelScaled")] - public JoinDataComplete SourceVolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "Source Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Source Volume Mute On Set / Get - /// - [JoinName("SourceVolumeMuteOn")] - public JoinDataComplete SourceVolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Source Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Source Volume Mute Off Set / Get - /// - [JoinName("SourceVolumeMuteOff")] - public JoinDataComplete SourceVolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "Source Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Source Volume Level Up - /// - [JoinName("SourceVolumeUp")] - public JoinDataComplete SourceVolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "Source Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Source Volume Level Down - /// - [JoinName("SourceVolumeDown")] - public JoinDataComplete SourceVolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "Source Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Source Volume Scaled Send Enable/Disable - /// - [JoinName("SourceVolumeLevelScaledSend")] - public JoinDataComplete SourceVolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "Source Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec1 Volume Signed dB Set / Get - /// - [JoinName("Codec1VolumeLevel")] - public JoinDataComplete Codec1VolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec1 Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Codec1 Volume 16bit Scaled Set / Get - /// - [JoinName("Codec1VolumeLevelScaled")] - public JoinDataComplete Codec1VolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec1 Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Codec1 Volume Mute On Set / Get - /// - [JoinName("Codec1VolumeMuteOn")] - public JoinDataComplete Codec1VolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec1 Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec1 Volume Mute Off Set / Get - /// - [JoinName("Codec1VolumeMuteOff")] - public JoinDataComplete Codec1VolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec1 Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec1 Volume Level Up - /// - [JoinName("Codec1VolumeUp")] - public JoinDataComplete Codec1VolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec1 Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec1 Volume Level Down - /// - [JoinName("Codec1VolumeDown")] - public JoinDataComplete Codec1VolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec1 Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec1 Volume Scaled Send Enable/Disable - /// - [JoinName("Codec1VolumeLevelScaledSend")] - public JoinDataComplete Codec1VolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec1 Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec2 Volume Signed dB Set / Get - /// - [JoinName("Codec2VolumeLevel")] - public JoinDataComplete Codec2VolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec2 Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Codec2 Volume 16bit Scaled Set / Get - /// - [JoinName("Codec2VolumeLevelScaled")] - public JoinDataComplete Codec2VolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec2 Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Codec2 Volume Mute On Set / Get - /// - [JoinName("Codec2VolumeMuteOn")] - public JoinDataComplete Codec2VolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec2 Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec2 Volume Mute Off Set / Get - /// - [JoinName("Codec2VolumeMuteOff")] - public JoinDataComplete Codec2VolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec2 Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec2 Volume Level Up - /// - [JoinName("Codec2VolumeUp")] - public JoinDataComplete Codec2VolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 33, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec2 Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec2 Volume Level Down - /// - [JoinName("Codec2VolumeDown")] - public JoinDataComplete Codec2VolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 34, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec2 Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Codec2 Volume Scaled Send Enable/Disable - /// - [JoinName("Codec2VolumeLevelScaledSend")] - public JoinDataComplete Codec2VolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 35, JoinSpan = 1 }, - new JoinMetadata { Description = "Codec2 Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// MicsMaster Volume Signed dB Set / Get - /// - [JoinName("MicsMasterVolumeLevel")] - public JoinDataComplete MicsMasterVolumeLevel = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, - new JoinMetadata { Description = "MicsMaster Volume Signed dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// MicsMaster Volume 16bit Scaled Set / Get - /// - [JoinName("MicsMasterVolumeLevelScaled")] - public JoinDataComplete MicsMasterVolumeLevelScaled = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, - new JoinMetadata { Description = "MicsMaster Volume 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// MicsMaster Volume Mute On Set / Get - /// - [JoinName("MicsMasterVolumeMuteOn")] - public JoinDataComplete MicsMasterVolumeMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, - new JoinMetadata { Description = "MicsMaster Volume Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// MicsMaster Volume Mute Off Set / Get - /// - [JoinName("MicsMasterVolumeMuteOff")] - public JoinDataComplete MicsMasterVolumeMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, - new JoinMetadata { Description = "MicsMaster Volume Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// MicsMaster Volume Level Up - /// - [JoinName("MicsMasterVolumeUp")] - public JoinDataComplete MicsMasterVolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, - new JoinMetadata { Description = "MicsMaster Volume Level Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// MicsMaster Volume Level Down - /// - [JoinName("MicsMasterVolumeDown")] - public JoinDataComplete MicsMasterVolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 44, JoinSpan = 1 }, - new JoinMetadata { Description = "MicsMaster Volume Level Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// MicsMaster Volume Scaled Send Enable/Disable - /// - [JoinName("MicsMasterVolumeLevelScaledSend")] - public JoinDataComplete MicsMasterVolumeLevelScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 45, JoinSpan = 1 }, - new JoinMetadata { Description = "Mics Master Volume Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public DmpsAudioOutputControllerJoinMap(uint joinStart) - : this(joinStart, typeof(DmpsAudioOutputControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected DmpsAudioOutputControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected DmpsAudioOutputControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsMicrophoneControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsMicrophoneControllerJoinMap.cs index 3fba12e5..90efc964 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsMicrophoneControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsMicrophoneControllerJoinMap.cs @@ -1,72 +1,49 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class DmpsMicrophoneControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("MicGain")] + public JoinDataComplete MicGain = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Mic Gain dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("MicGainScaled")] + public JoinDataComplete MicGainScaled = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Mic Gain 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("MicMuteOn")] + public JoinDataComplete MicMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Mic Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MicMuteOff")] + public JoinDataComplete MicMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Mic Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MicGainScaledSend")] + public JoinDataComplete MicGainScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Mic Gain Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("MicName")] + public JoinDataComplete MicName = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Mic Name Get", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + /// - /// Represents a DmpsMicrophoneControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class DmpsMicrophoneControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public DmpsMicrophoneControllerJoinMap(uint joinStart) + : this(joinStart, typeof(DmpsMicrophoneControllerJoinMap)) { + } - /// - /// Mic Gain dB Set / Get - /// - [JoinName("MicGain")] - public JoinDataComplete MicGain = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Mic Gain dB Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Mic Gain 16bit Scaled Set / Get - /// - [JoinName("MicGainScaled")] - public JoinDataComplete MicGainScaled = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Mic Gain 16bit Scaled Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Mic Mute On Set / Get - /// - [JoinName("MicMuteOn")] - public JoinDataComplete MicMuteOn = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Mic Mute On Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Mic Mute Off Set / Get - /// - [JoinName("MicMuteOff")] - public JoinDataComplete MicMuteOff = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Mic Mute Off Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Mic Gain Scaled Send Enable/Disable - /// - [JoinName("MicGainScaledSend")] - public JoinDataComplete MicGainScaledSend = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Mic Gain Scaled Send Enable/Disable", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Mic Name Get - /// - [JoinName("MicName")] - public JoinDataComplete MicName = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Mic Name Get", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public DmpsMicrophoneControllerJoinMap(uint joinStart) - : this(joinStart, typeof(DmpsMicrophoneControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected DmpsMicrophoneControllerJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected DmpsMicrophoneControllerJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsRoutingControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsRoutingControllerJoinMap.cs index 8e9a3e97..e39e38c6 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsRoutingControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/DmpsRoutingControllerJoinMap.cs @@ -1,185 +1,122 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class DmpsRoutingControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("EnableRouting")] + public JoinDataComplete EnableRouting = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "DMPS Enable Audio and Video Routing", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SystemPowerOn")] + public JoinDataComplete SystemPowerOn = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "DMPS System Power On Get/Set", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SystemPowerOff")] + public JoinDataComplete SystemPowerOff = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "DMPS System Power Off Get/Set", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("FrontPanelLockOn")] + public JoinDataComplete FrontPanelLockOn = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "DMPS Front Panel Lock On Get/Set", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("FrontPanelLockOff")] + public JoinDataComplete FrontPanelLockOff = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "DMPS Front Panel Lock Off Get/Set", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoSyncStatus")] + public JoinDataComplete VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Input Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("InputEndpointOnline")] + public JoinDataComplete InputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutputEndpointOnline")] + public JoinDataComplete OutputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutputVideo")] + public JoinDataComplete OutputVideo = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Video Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("OutputAudio")] + public JoinDataComplete OutputAudio = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Audio Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("InputNames")] + public JoinDataComplete InputNames = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("OutputNames")] + public JoinDataComplete OutputNames = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Output Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("InputVideoNames")] + public JoinDataComplete InputVideoNames = + new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 32 }, + new JoinMetadata + { + Description = "Video Input Name", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("InputAudioNames")] + public JoinDataComplete InputAudioNames = + new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 32 }, + new JoinMetadata + { + Description = "Audio Input Name", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + [JoinName("OutputVideoNames")] + public JoinDataComplete OutputVideoNames = + new JoinDataComplete(new JoinData { JoinNumber = 901, JoinSpan = 32 }, + new JoinMetadata + { + Description = "Video Output Name", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + [JoinName("OutputAudioNames")] + public JoinDataComplete OutputAudioNames = + new JoinDataComplete(new JoinData { JoinNumber = 1101, JoinSpan = 32 }, + new JoinMetadata + { + Description = "Audio Output Name", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("OutputCurrentVideoInputNames")] + public JoinDataComplete OutputCurrentVideoInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2001, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Video Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("OutputCurrentAudioInputNames")] + public JoinDataComplete OutputCurrentAudioInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2201, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Audio Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("InputCurrentResolution")] + public JoinDataComplete InputCurrentResolution = new JoinDataComplete(new JoinData { JoinNumber = 2401, JoinSpan = 32 }, + new JoinMetadata { Description = "DM Chassis Input Current Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + /// - /// Represents a DmpsRoutingControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class DmpsRoutingControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public DmpsRoutingControllerJoinMap(uint joinStart) + : this(joinStart, typeof(DmpsRoutingControllerJoinMap)) { - /// - /// DMPS Enable Audio and Video Routing - /// - [JoinName("EnableRouting")] - public JoinDataComplete EnableRouting = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "DMPS Enable Audio and Video Routing", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DMPS Disable Audio and Video Routing - /// - [JoinName("SystemPowerOn")] - public JoinDataComplete SystemPowerOn = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "DMPS System Power On Get/Set", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DMPS Disable Audio and Video Routing - /// - [JoinName("SystemPowerOff")] - public JoinDataComplete SystemPowerOff = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "DMPS System Power Off Get/Set", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DMPS Front Panel Lock On Get/Set - /// - [JoinName("FrontPanelLockOn")] - public JoinDataComplete FrontPanelLockOn = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "DMPS Front Panel Lock On Get/Set", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DMPS Front Panel Lock Off Get/Set - /// - [JoinName("FrontPanelLockOff")] - public JoinDataComplete FrontPanelLockOff = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "DMPS Front Panel Lock Off Get/Set", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Input Video Sync - /// - [JoinName("VideoSyncStatus")] - public JoinDataComplete VideoSyncStatus = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Input Video Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Chassis Input Endpoint Online - /// - [JoinName("InputEndpointOnline")] - public JoinDataComplete InputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Chassis Output Endpoint Online - /// - [JoinName("OutputEndpointOnline")] - public JoinDataComplete OutputEndpointOnline = new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Endpoint Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DM Chassis Input Video Set / Get - /// - [JoinName("OutputVideo")] - public JoinDataComplete OutputVideo = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Video Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Input Audio Set / Get - /// - [JoinName("OutputAudio")] - public JoinDataComplete OutputAudio = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Audio Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// DM Chassis Input Name - /// - [JoinName("InputNames")] - public JoinDataComplete InputNames = new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Output Name - /// - [JoinName("OutputNames")] - public JoinDataComplete OutputNames = new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Output Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Video Input Name - /// - [JoinName("InputVideoNames")] - public JoinDataComplete InputVideoNames = - new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 32 }, - new JoinMetadata - { - Description = "DM Chassis Video Input Name", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); - - /// - /// DM Chassis Audio Input Name - /// - [JoinName("InputAudioNames")] - public JoinDataComplete InputAudioNames = - new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 32 }, - new JoinMetadata - { - Description = "Audio Input Name", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); - - /// - /// DM Chassis Video Output Name - /// - [JoinName("OutputVideoNames")] - public JoinDataComplete OutputVideoNames = - new JoinDataComplete(new JoinData { JoinNumber = 901, JoinSpan = 32 }, - new JoinMetadata - { - Description = "Video Output Name", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); - - /// - /// DM Chassis Audio Output Name - /// - [JoinName("OutputAudioNames")] - public JoinDataComplete OutputAudioNames = - new JoinDataComplete(new JoinData { JoinNumber = 1101, JoinSpan = 32 }, - new JoinMetadata - { - Description = "Audio Output Name", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); - - /// - /// DM Chassis Video Output Currently Routed Video Input Name - /// - [JoinName("OutputCurrentVideoInputNames")] - public JoinDataComplete OutputCurrentVideoInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2001, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Video Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Audio Output Currently Routed Video Input Name - /// - [JoinName("OutputCurrentAudioInputNames")] - public JoinDataComplete OutputCurrentAudioInputNames = new JoinDataComplete(new JoinData { JoinNumber = 2201, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Audio Output Currently Routed Video Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// DM Chassis Input Current Resolution - /// - [JoinName("InputCurrentResolution")] - public JoinDataComplete InputCurrentResolution = new JoinDataComplete(new JoinData { JoinNumber = 2401, JoinSpan = 32 }, - new JoinMetadata { Description = "DM Chassis Input Current Resolution", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + } - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public DmpsRoutingControllerJoinMap(uint joinStart) - : this(joinStart, typeof(DmpsRoutingControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected DmpsRoutingControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected DmpsRoutingControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericIrControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericIrControllerJoinMap.cs index 57977737..00087187 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericIrControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericIrControllerJoinMap.cs @@ -1,10 +1,7 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.Bridges.JoinMaps -{ - /// - /// Represents a GenericIrControllerJoinMap - /// +namespace PepperDash.Essentials.Core.Bridges.JoinMaps; + public sealed class GenericIrControllerJoinMap : JoinMapBaseAdvanced { /// @@ -995,5 +992,4 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps : base(joinStart, typeof(GenericIrControllerJoinMap)) { } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericLightingJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericLightingJoinMap.cs index 2b684d9f..179b4124 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericLightingJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericLightingJoinMap.cs @@ -1,67 +1,48 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class GenericLightingJoinMap : JoinMapBaseAdvanced { + + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Lighting Controller Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SelectScene")] + public JoinDataComplete SelectScene = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Lighting Controller Select Scene By Index", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SelectSceneDirect")] + public JoinDataComplete SelectSceneDirect = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 10 }, + new JoinMetadata { Description = "Lighting Controller Select Scene", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.DigitalSerial }); + + [JoinName("ButtonVisibility")] + public JoinDataComplete ButtonVisibility = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 10 }, + new JoinMetadata { Description = "Lighting Controller Button Visibility", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IntegrationIdSet")] + public JoinDataComplete IntegrationIdSet = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Lighting Controller Set Integration Id", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial }); + + + /// - /// Represents a GenericLightingJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class GenericLightingJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public GenericLightingJoinMap(uint joinStart) + : this(joinStart, typeof(GenericLightingJoinMap)) { + } - /// - /// Lighting Controller Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Lighting Controller Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Select Scene By Index - /// - [JoinName("SelectScene")] - public JoinDataComplete SelectScene = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Lighting Controller Select Scene By Index", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Select Scene Direct - /// - [JoinName("SelectSceneDirect")] - public JoinDataComplete SelectSceneDirect = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 10 }, - new JoinMetadata { Description = "Lighting Controller Select Scene", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.DigitalSerial }); - - /// - /// Button Visibility - /// - [JoinName("ButtonVisibility")] - public JoinDataComplete ButtonVisibility = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 10 }, - new JoinMetadata { Description = "Lighting Controller Button Visibility", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Set Integration Id - /// - [JoinName("IntegrationIdSet")] - public JoinDataComplete IntegrationIdSet = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Lighting Controller Set Integration Id", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial }); - - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public GenericLightingJoinMap(uint joinStart) - : this(joinStart, typeof(GenericLightingJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected GenericLightingJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected GenericLightingJoinMap(uint joinStart, Type type) : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericRelayControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericRelayControllerJoinMap.cs index 470dc4e1..03aaf041 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericRelayControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GenericRelayControllerJoinMap.cs @@ -1,37 +1,30 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class GenericRelayControllerJoinMap : JoinMapBaseAdvanced { + + [JoinName("Relay")] + public JoinDataComplete Relay = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Relay State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + /// - /// Represents a GenericRelayControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class GenericRelayControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public GenericRelayControllerJoinMap(uint joinStart) + : this(joinStart, typeof(GenericRelayControllerJoinMap)) { + } - /// - /// Device Relay State Set / Get - /// - [JoinName("Relay")] - public JoinDataComplete Relay = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Relay State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public GenericRelayControllerJoinMap(uint joinStart) - : this(joinStart, typeof(GenericRelayControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected GenericRelayControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected GenericRelayControllerJoinMap(uint joinStart, Type type) : base(joinStart, type) + { + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GlsOccupancySensorBaseJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GlsOccupancySensorBaseJoinMap.cs index 23341e7e..228cb4f0 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GlsOccupancySensorBaseJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GlsOccupancySensorBaseJoinMap.cs @@ -1,304 +1,184 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class GlsOccupancySensorBaseJoinMap : JoinMapBaseAdvanced { + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Is Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ForceOccupied")] + public JoinDataComplete ForceOccupied = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Set to Occupied", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ForceVacant")] + public JoinDataComplete ForceVacant = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Set to Vacant", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableRawStates")] + public JoinDataComplete EnableRawStates = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Enable Raw", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RoomOccupiedFeedback")] + public JoinDataComplete RoomOccupiedFeedback = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Room Is Occupied", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("GraceOccupancyDetectedFeedback")] + public JoinDataComplete GraceOccupancyDetectedFeedback = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Grace Occupancy Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RoomVacantFeedback")] + public JoinDataComplete RoomVacantFeedback = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Room Is Vacant", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RawOccupancyFeedback")] + public JoinDataComplete RawOccupancyFeedback = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Raw Occupancy Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RawOccupancyPirFeedback")] + public JoinDataComplete RawOccupancyPirFeedback = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Raw PIR Occupancy Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RawOccupancyUsFeedback")] + public JoinDataComplete RawOccupancyUsFeedback = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Raw US Occupancy Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableLedFlash")] + public JoinDataComplete EnableLedFlash = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Enable LED Flash", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableLedFlash")] + public JoinDataComplete DisableLedFlash = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Disable LED Flash", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableShortTimeout")] + public JoinDataComplete EnableShortTimeout = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Enable Short Timeout", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableShortTimeout")] + public JoinDataComplete DisableShortTimeout = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Disable Short Timeout", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OrWhenVacated")] + public JoinDataComplete OrWhenVacated = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Set To Vacant when Either Sensor is Vacant", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("AndWhenVacated")] + public JoinDataComplete AndWhenVacated = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Set To Vacant when Both Sensors are Vacant", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableUsA")] + public JoinDataComplete EnableUsA = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Enable Ultrasonic Sensor A", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableUsA")] + public JoinDataComplete DisableUsA = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Disable Ultrasonic Sensor A", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnableUsB")] + public JoinDataComplete EnableUsB = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Enable Ultrasonic Sensor B", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableUsB")] + public JoinDataComplete DisableUsB = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Disable Ultrasonic Sensor B", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("EnablePir")] + public JoinDataComplete EnablePir = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Enable IR Sensor", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisablePir")] + public JoinDataComplete DisablePir = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Disable IR Sensor", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IncrementUsInOccupiedState")] + public JoinDataComplete IncrementUsInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Increment US Occupied State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DecrementUsInOccupiedState")] + public JoinDataComplete DecrementUsInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Decrement US Occupied State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IncrementUsInVacantState")] + public JoinDataComplete IncrementUsInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Increment US Vacant State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DecrementUsInVacantState")] + public JoinDataComplete DecrementUsInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Decrement US Vacant State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IncrementPirInOccupiedState")] + public JoinDataComplete IncrementPirInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Increment IR Occupied State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DecrementPirInOccupiedState")] + public JoinDataComplete DecrementPirInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Decrement IR Occupied State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IncrementPirInVacantState")] + public JoinDataComplete IncrementPirInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Increment IR Vacant State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DecrementPirInVacantState")] + public JoinDataComplete DecrementPirInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Decrement IR Vacant State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Timeout")] + public JoinDataComplete Timeout = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Timeout Value", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("TimeoutLocalFeedback")] + public JoinDataComplete TimeoutLocalFeedback = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Local Timeout Value", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("InternalPhotoSensorValue")] + public JoinDataComplete InternalPhotoSensorValue = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Internal PhotoSensor Value", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("ExternalPhotoSensorValue")] + public JoinDataComplete ExternalPhotoSensorValue = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor External PhotoSensor Value", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("UsSensitivityInOccupiedState")] + public JoinDataComplete UsSensitivityInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Ultrasonic Sensitivity in Occupied State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("UsSensitivityInVacantState")] + public JoinDataComplete UsSensitivityInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Ultrasonic Sensitivity in Vacant State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("PirSensitivityInOccupiedState")] + public JoinDataComplete PirSensitivityInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor PIR Sensitivity in Occupied State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("PirSensitivityInVacantState")] + public JoinDataComplete PirSensitivityInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor PIR Sensitivity in Vacant State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Occ Sensor Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + /// - /// Represents a GlsOccupancySensorBaseJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class GlsOccupancySensorBaseJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public GlsOccupancySensorBaseJoinMap(uint joinStart) + : this(joinStart, typeof(GlsOccupancySensorBaseJoinMap)) { - /// - /// Occ Sensor Is Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Is Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Set to Occupied - /// - [JoinName("ForceOccupied")] - public JoinDataComplete ForceOccupied = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Set to Occupied", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Set to Vacant - /// - [JoinName("ForceVacant")] - public JoinDataComplete ForceVacant = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Set to Vacant", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Enable Raw - /// - [JoinName("EnableRawStates")] - public JoinDataComplete EnableRawStates = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Enable Raw", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Disable Raw - /// - [JoinName("RoomOccupiedFeedback")] - public JoinDataComplete RoomOccupiedFeedback = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Room Is Occupied", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Grace Occupancy Detected - /// - [JoinName("GraceOccupancyDetectedFeedback")] - public JoinDataComplete GraceOccupancyDetectedFeedback = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Grace Occupancy Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Room Is Vacant - /// - [JoinName("RoomVacantFeedback")] - public JoinDataComplete RoomVacantFeedback = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Room Is Vacant", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Raw Occupancy Detected - /// - [JoinName("RawOccupancyFeedback")] - public JoinDataComplete RawOccupancyFeedback = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Raw Occupancy Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Raw PIR Occupancy Detected - /// - [JoinName("RawOccupancyPirFeedback")] - public JoinDataComplete RawOccupancyPirFeedback = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Raw PIR Occupancy Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Raw US Occupancy Detected - /// - [JoinName("RawOccupancyUsFeedback")] - public JoinDataComplete RawOccupancyUsFeedback = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Raw US Occupancy Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Enable LED Flash - /// - [JoinName("EnableLedFlash")] - public JoinDataComplete EnableLedFlash = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Enable LED Flash", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Disable LED Flash - /// - [JoinName("DisableLedFlash")] - public JoinDataComplete DisableLedFlash = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Disable LED Flash", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Enable Short Timeout - /// - [JoinName("EnableShortTimeout")] - public JoinDataComplete EnableShortTimeout = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Enable Short Timeout", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Disable Short Timeout - /// - [JoinName("DisableShortTimeout")] - public JoinDataComplete DisableShortTimeout = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Disable Short Timeout", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Set To Vacant when Either Sensor is Vacant - /// - [JoinName("OrWhenVacated")] - public JoinDataComplete OrWhenVacated = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Set To Vacant when Either Sensor is Vacant", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Set To Vacant when Both Sensors are Vacant - /// - [JoinName("AndWhenVacated")] - public JoinDataComplete AndWhenVacated = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Set To Vacant when Both Sensors are Vacant", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Enable Ultrasonic Sensor A - /// - [JoinName("EnableUsA")] - public JoinDataComplete EnableUsA = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Enable Ultrasonic Sensor A", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Disable Ultrasonic Sensor A - /// - [JoinName("DisableUsA")] - public JoinDataComplete DisableUsA = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Disable Ultrasonic Sensor A", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Enable Ultrasonic Sensor B - /// - [JoinName("EnableUsB")] - public JoinDataComplete EnableUsB = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Enable Ultrasonic Sensor B", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Disable Ultrasonic Sensor B - /// - [JoinName("DisableUsB")] - public JoinDataComplete DisableUsB = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Disable Ultrasonic Sensor B", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Enable IR Sensor - /// - [JoinName("EnablePir")] - public JoinDataComplete EnablePir = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Enable IR Sensor", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Disable IR Sensor - /// - [JoinName("DisablePir")] - public JoinDataComplete DisablePir = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Disable IR Sensor", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Increment US Occupied State Sensitivity - /// - [JoinName("IncrementUsInOccupiedState")] - public JoinDataComplete IncrementUsInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Increment US Occupied State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Decrement US Occupied State Sensitivity - /// - [JoinName("DecrementUsInOccupiedState")] - public JoinDataComplete DecrementUsInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Decrement US Occupied State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Increment US Vacant State Sensitivity - /// - [JoinName("IncrementUsInVacantState")] - public JoinDataComplete IncrementUsInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Increment US Vacant State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Decrement US Vacant State Sensitivity - /// - [JoinName("DecrementUsInVacantState")] - public JoinDataComplete DecrementUsInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Decrement US Vacant State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Increment IR Occupied State Sensitivity - /// - [JoinName("IncrementPirInOccupiedState")] - public JoinDataComplete IncrementPirInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Increment IR Occupied State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Decrement IR Occupied State Sensitivity - /// - [JoinName("DecrementPirInOccupiedState")] - public JoinDataComplete DecrementPirInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Decrement IR Occupied State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Increment IR Vacant State Sensitivity - /// - [JoinName("IncrementPirInVacantState")] - public JoinDataComplete IncrementPirInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Increment IR Vacant State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Decrement IR Vacant State Sensitivity - /// - [JoinName("DecrementPirInVacantState")] - public JoinDataComplete DecrementPirInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Decrement IR Vacant State Sensitivity", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Occ Sensor Timeout Value - /// - [JoinName("Timeout")] - public JoinDataComplete Timeout = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Timeout Value", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Occ Sensor Local Timeout Value - /// - [JoinName("TimeoutLocalFeedback")] - public JoinDataComplete TimeoutLocalFeedback = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Local Timeout Value", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Occ Sensor Internal PhotoSensor Value - /// - [JoinName("InternalPhotoSensorValue")] - public JoinDataComplete InternalPhotoSensorValue = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Internal PhotoSensor Value", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Occ Sensor External PhotoSensor Value - /// - [JoinName("ExternalPhotoSensorValue")] - public JoinDataComplete ExternalPhotoSensorValue = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor External PhotoSensor Value", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Occ Sensor Ultrasonic Sensitivity in Occupied State - /// - [JoinName("UsSensitivityInOccupiedState")] - public JoinDataComplete UsSensitivityInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Ultrasonic Sensitivity in Occupied State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Occ Sensor Ultrasonic Sensitivity in Vacant State - /// - [JoinName("UsSensitivityInVacantState")] - public JoinDataComplete UsSensitivityInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Ultrasonic Sensitivity in Vacant State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Occ Sensor PIR Sensitivity in Occupied State - /// - [JoinName("PirSensitivityInOccupiedState")] - public JoinDataComplete PirSensitivityInOccupiedState = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor PIR Sensitivity in Occupied State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Occ Sensor PIR Sensitivity in Vacant State - /// - [JoinName("PirSensitivityInVacantState")] - public JoinDataComplete PirSensitivityInVacantState = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor PIR Sensitivity in Vacant State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Occ Sensor Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Occ Sensor Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public GlsOccupancySensorBaseJoinMap(uint joinStart) - : this(joinStart, typeof(GlsOccupancySensorBaseJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected GlsOccupancySensorBaseJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } - } -} + + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected GlsOccupancySensorBaseJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { + } + +} + diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GlsPartitionSensorJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GlsPartitionSensorJoinMap.cs index 98c491dd..7515c8c2 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GlsPartitionSensorJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/GlsPartitionSensorJoinMap.cs @@ -1,183 +1,156 @@ using System; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.Bridges.JoinMaps +namespace PepperDash.Essentials.Core.Bridges.JoinMaps; + +public class GlsPartitionSensorJoinMap : JoinMapBaseAdvanced { + + #region Digital + + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete( + new JoinData + { + JoinNumber = 1, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sensor Is Online", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + + [JoinName("Enable")] + public JoinDataComplete Enable = new JoinDataComplete( + new JoinData + { + JoinNumber = 2, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sensor Enable", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("PartitionSensed")] + public JoinDataComplete PartitionSensed = new JoinDataComplete( + new JoinData + { + JoinNumber = 3, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sensor Partition Sensed", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("PartitionNotSensed")] + public JoinDataComplete PartitionNotSensed = new JoinDataComplete( + new JoinData + { + JoinNumber = 4, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sensor Partition Not Sensed", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("IncreaseSensitivity")] + public JoinDataComplete IncreaseSensitivity = new JoinDataComplete( + new JoinData + { + JoinNumber = 6, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sensor Increase Sensitivity", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DecreaseSensitivity")] + public JoinDataComplete DecreaseSensitivity = new JoinDataComplete( + new JoinData + { + JoinNumber = 7, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sensor Decrease Sensitivity", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + #endregion + + #region Analog + + [JoinName("Sensitivity")] + public JoinDataComplete Sensitivity = new JoinDataComplete( + new JoinData + { + JoinNumber = 2, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sensor Sensitivity", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Analog + }); + + #endregion + + + #region Serial + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete( + new JoinData + { + JoinNumber = 1, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sensor Name", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + + #endregion + + /// - /// Represents a GlsPartitionSensorJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class GlsPartitionSensorJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public GlsPartitionSensorJoinMap(uint joinStart) + : this(joinStart, typeof(GlsPartitionSensorJoinMap)) { - #region Digital + } - /// - /// Sensor Is Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete( - new JoinData - { - JoinNumber = 1, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sensor Is Online", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Digital - }); + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected GlsPartitionSensorJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { - /// - /// Sensor Enable - /// - [JoinName("Enable")] - public JoinDataComplete Enable = new JoinDataComplete( - new JoinData - { - JoinNumber = 2, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sensor Enable", - JoinCapabilities = eJoinCapabilities.ToFromSIMPL, - JoinType = eJoinType.Digital - }); - - /// - /// Sensor Partition Sensed - /// - [JoinName("PartitionSensed")] - public JoinDataComplete PartitionSensed = new JoinDataComplete( - new JoinData - { - JoinNumber = 3, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sensor Partition Sensed", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Digital - }); - - /// - /// Sensor Partition Not Sensed - /// - [JoinName("PartitionNotSensed")] - public JoinDataComplete PartitionNotSensed = new JoinDataComplete( - new JoinData - { - JoinNumber = 4, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sensor Partition Not Sensed", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Digital - }); - - /// - /// Sensor Increase Sensitivity - /// - [JoinName("IncreaseSensitivity")] - public JoinDataComplete IncreaseSensitivity = new JoinDataComplete( - new JoinData - { - JoinNumber = 6, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sensor Increase Sensitivity", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); - - /// - /// Sensor Decrease Sensitivity - /// - [JoinName("DecreaseSensitivity")] - public JoinDataComplete DecreaseSensitivity = new JoinDataComplete( - new JoinData - { - JoinNumber = 7, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sensor Decrease Sensitivity", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); - - #endregion - - #region Analog - - /// - /// Sensor Sensitivity - /// - [JoinName("Sensitivity")] - public JoinDataComplete Sensitivity = new JoinDataComplete( - new JoinData - { - JoinNumber = 2, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sensor Sensitivity", - JoinCapabilities = eJoinCapabilities.ToFromSIMPL, - JoinType = eJoinType.Analog - }); - - #endregion - - - #region Serial - - /// - /// Sensor Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete( - new JoinData - { - JoinNumber = 1, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sensor Name", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); - - #endregion - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public GlsPartitionSensorJoinMap(uint joinStart) - : this(joinStart, typeof(GlsPartitionSensorJoinMap)) - { - - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected GlsPartitionSensorJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - - } } } diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdMdNxM4kEControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdMdNxM4kEControllerJoinMap.cs index 950e3f1e..43ed25c9 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdMdNxM4kEControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdMdNxM4kEControllerJoinMap.cs @@ -1,99 +1,65 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class HdMdNxM4kEControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("EnableAutoRoute")] + public JoinDataComplete EnableAutoRoute = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Enable Automatic Routing on 4x1 Switchers", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("InputName")] + public JoinDataComplete InputName = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 8 }, + new JoinMetadata { Description = "Device Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("InputSync")] + public JoinDataComplete InputSync = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 8 }, + new JoinMetadata { Description = "Device Input Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutputName")] + public JoinDataComplete OutputName = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 2 }, + new JoinMetadata { Description = "Device Output Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("OutputRoute")] + public JoinDataComplete OutputRoute = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 2 }, + new JoinMetadata { Description = "Device Output Route Set/Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("OutputRoutedName")] + public JoinDataComplete OutputRoutedName = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 2 }, + new JoinMetadata { Description = "Device Output Route Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("EnableInputHdcp")] + public JoinDataComplete EnableInputHdcp = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 8 }, + new JoinMetadata { Description = "Device Enable Input Hdcp", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DisableInputHdcp")] + public JoinDataComplete DisableInputHdcp = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 8 }, + new JoinMetadata { Description = "Device Disnable Input Hdcp", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Onlne", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + /// - /// Represents a HdMdNxM4kEControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class HdMdNxM4kEControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public HdMdNxM4kEControllerJoinMap(uint joinStart) + : this(joinStart, typeof(HdMdNxM4kEControllerJoinMap)) { - /// - /// Device Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + } - /// - /// Enable Automatic Routing on 4x1 Switchers - /// - [JoinName("EnableAutoRoute")] - public JoinDataComplete EnableAutoRoute = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Enable Automatic Routing on 4x1 Switchers", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Device Input Name - /// - [JoinName("InputName")] - public JoinDataComplete InputName = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 8 }, - new JoinMetadata { Description = "Device Input Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Device Input Sync - /// - [JoinName("InputSync")] - public JoinDataComplete InputSync = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 8 }, - new JoinMetadata { Description = "Device Input Sync", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Device Output Name - /// - [JoinName("OutputName")] - public JoinDataComplete OutputName = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 2 }, - new JoinMetadata { Description = "Device Output Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Device Output Route Set/Get - /// - [JoinName("OutputRoute")] - public JoinDataComplete OutputRoute = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 2 }, - new JoinMetadata { Description = "Device Output Route Set/Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Device Output Route Name - /// - [JoinName("OutputRoutedName")] - public JoinDataComplete OutputRoutedName = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 2 }, - new JoinMetadata { Description = "Device Output Route Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Device Enable Input Hdcp - /// - [JoinName("EnableInputHdcp")] - public JoinDataComplete EnableInputHdcp = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 8 }, - new JoinMetadata { Description = "Device Enable Input Hdcp", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Device Disable Input Hdcp - /// - [JoinName("DisableInputHdcp")] - public JoinDataComplete DisableInputHdcp = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 8 }, - new JoinMetadata { Description = "Device Disnable Input Hdcp", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Device Online Status - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Onlne", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public HdMdNxM4kEControllerJoinMap(uint joinStart) - : this(joinStart, typeof(HdMdNxM4kEControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected HdMdNxM4kEControllerJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected HdMdNxM4kEControllerJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdMdxxxCEControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdMdxxxCEControllerJoinMap.cs index 2730a0ea..32bc5651 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdMdxxxCEControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdMdxxxCEControllerJoinMap.cs @@ -1,114 +1,75 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class HdMdxxxCEControllerJoinMap : JoinMapBaseAdvanced { + + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RemoteEndDetected")] + public JoinDataComplete RemoteEndDetected = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Remote End Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("AutoRouteOn")] + public JoinDataComplete AutoRouteOn = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Auto Route On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("AutoRouteOff")] + public JoinDataComplete AutoRouteOff = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Auto Route Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PriorityRoutingOn")] + public JoinDataComplete PriorityRoutingOn = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Priority Routing On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PriorityRoutingOff")] + public JoinDataComplete PriorityRoutingOff = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Priority Routing Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("InputOnScreenDisplayEnabled")] + public JoinDataComplete InputOnScreenDisplayEnabled = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Input OSD Enabled", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("InputOnScreenDisplayDisabled")] + public JoinDataComplete InputOnScreenDisplayDisabled = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "Device Input OSD Disabled", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("SyncDetected")] + public JoinDataComplete SyncDetected = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 5 }, + new JoinMetadata { Description = "Device Sync Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VideoSource")] + public JoinDataComplete VideoSource = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 5 }, + new JoinMetadata { Description = "Device Video Source Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("SourceCount")] + public JoinDataComplete SourceCount = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 5 }, + new JoinMetadata { Description = "Device Video Source Count", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("SourceNames")] + public JoinDataComplete SourceNames = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 5 }, + new JoinMetadata { Description = "Device Video Source Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + /// - /// Represents a HdMdxxxCEControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class HdMdxxxCEControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public HdMdxxxCEControllerJoinMap(uint joinStart) + : this(joinStart, typeof(HdMdxxxCEControllerJoinMap)) { + } - /// - /// Device Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Remote End Detected - /// - [JoinName("RemoteEndDetected")] - public JoinDataComplete RemoteEndDetected = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Remote End Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Auto Route On - /// - [JoinName("AutoRouteOn")] - public JoinDataComplete AutoRouteOn = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Auto Route On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Auto Route Off - /// - [JoinName("AutoRouteOff")] - public JoinDataComplete AutoRouteOff = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Auto Route Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Priority Routing On - /// - [JoinName("PriorityRoutingOn")] - public JoinDataComplete PriorityRoutingOn = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Priority Routing On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Priority Routing Off - /// - [JoinName("PriorityRoutingOff")] - public JoinDataComplete PriorityRoutingOff = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Priority Routing Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Input On Screen Display Enabled - /// - [JoinName("InputOnScreenDisplayEnabled")] - public JoinDataComplete InputOnScreenDisplayEnabled = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Input OSD Enabled", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Input On Screen Display Disabled - /// - [JoinName("InputOnScreenDisplayDisabled")] - public JoinDataComplete InputOnScreenDisplayDisabled = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "Device Input OSD Disabled", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Sync Detected - /// - [JoinName("SyncDetected")] - public JoinDataComplete SyncDetected = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 5 }, - new JoinMetadata { Description = "Device Sync Detected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Video Source - /// - [JoinName("VideoSource")] - public JoinDataComplete VideoSource = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 5 }, - new JoinMetadata { Description = "Device Video Source Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Source Count - /// - [JoinName("SourceCount")] - public JoinDataComplete SourceCount = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 5 }, - new JoinMetadata { Description = "Device Video Source Count", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - /// - /// Source Names - /// - [JoinName("SourceNames")] - public JoinDataComplete SourceNames = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 5 }, - new JoinMetadata { Description = "Device Video Source Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public HdMdxxxCEControllerJoinMap(uint joinStart) - : this(joinStart, typeof(HdMdxxxCEControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected HdMdxxxCEControllerJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected HdMdxxxCEControllerJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdPsXxxControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdPsXxxControllerJoinMap.cs index aee3c2f1..e5225ac6 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdPsXxxControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/HdPsXxxControllerJoinMap.cs @@ -1,11 +1,8 @@ using System; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.Bridges -{ - /// - /// Represents a HdPsXxxControllerJoinMap - /// +namespace PepperDash.Essentials.Core.Bridges; + public class HdPsXxxControllerJoinMap : JoinMapBaseAdvanced { @@ -212,5 +209,4 @@ namespace PepperDash.Essentials.Core.Bridges : base(joinStart, type) { } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/Hrxxx0WirelessRemoteControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/Hrxxx0WirelessRemoteControllerJoinMap.cs index 5c4c858e..ca9ef3c6 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/Hrxxx0WirelessRemoteControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/Hrxxx0WirelessRemoteControllerJoinMap.cs @@ -1,414 +1,245 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class Hrxxx0WirelessRemoteControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("Power")] + public JoinDataComplete Power = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Power", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Menu")] + public JoinDataComplete Menu = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Menu", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Guide")] + public JoinDataComplete Guide = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Guide", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Info")] + public JoinDataComplete Info = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Info", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VolumeUp")] + public JoinDataComplete VolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "VolumeUp", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("VolumeDown")] + public JoinDataComplete VolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "VolumeDown", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DialPadUp")] + public JoinDataComplete DialPadUp = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "DialPadUp", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DialPadDown")] + public JoinDataComplete DialPadDown = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "DialPadDown", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DialPadLeft")] + public JoinDataComplete DialPadLeft = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, + new JoinMetadata { Description = "DialPadLeft", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DialPadRight")] + public JoinDataComplete DialPadRight = new JoinDataComplete(new JoinData { JoinNumber = 10, JoinSpan = 1 }, + new JoinMetadata { Description = "DialPadRight", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DialPadSelect")] + public JoinDataComplete DialPadSelect = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "DialPadSelect", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChannelUp")] + public JoinDataComplete ChannelUp = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "ChannelUp", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChannelDown")] + public JoinDataComplete ChannelDown = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "ChannelDown", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Mute")] + public JoinDataComplete Mute = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "Mute", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Exit")] + public JoinDataComplete Exit = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "Exit", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Last")] + public JoinDataComplete Last = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, + new JoinMetadata { Description = "Last", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Play")] + public JoinDataComplete Play = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, + new JoinMetadata { Description = "Play", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Pause")] + public JoinDataComplete Pause = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, + new JoinMetadata { Description = "Pause", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Rewind")] + public JoinDataComplete Rewind = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, + new JoinMetadata { Description = "Rewind", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("FastForward")] + public JoinDataComplete FastForward = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, + new JoinMetadata { Description = "FastForward", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PreviousTrack")] + public JoinDataComplete PreviousTrack = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata { Description = "PreviousTrack", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("NextTrack")] + public JoinDataComplete NextTrack = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata { Description = "NextTrack", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Stop")] + public JoinDataComplete Stop = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, + new JoinMetadata { Description = "Stop", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Record")] + public JoinDataComplete Record = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, + new JoinMetadata { Description = "Record", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Dvr")] + public JoinDataComplete Dvr = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, + new JoinMetadata { Description = "Dvr", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad1")] + public JoinDataComplete Keypad1 = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad1", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad2Abc")] + public JoinDataComplete Keypad2 = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad2Abc", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad3Def")] + public JoinDataComplete Keypad3Def = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad3Def", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad4Ghi")] + public JoinDataComplete Keypad4Ghi = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad4Ghi", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad5Jkl")] + public JoinDataComplete Keypad5Jkl = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad5Jkl", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad6Mno")] + public JoinDataComplete Keypad6Mno = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad6Mno", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad7Pqrs")] + public JoinDataComplete Keypad7Pqrs = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad7Pqrs", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad8Tuv")] + public JoinDataComplete Keypad8Tuv = new JoinDataComplete(new JoinData { JoinNumber = 33, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad8Tuv", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad9Wxyz")] + public JoinDataComplete Keypad9Wxyz = new JoinDataComplete(new JoinData { JoinNumber = 34, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad9Wxyz", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Keypad0")] + public JoinDataComplete Keypad0 = new JoinDataComplete(new JoinData { JoinNumber = 35, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad0", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Clear")] + public JoinDataComplete Clear = new JoinDataComplete(new JoinData { JoinNumber = 36, JoinSpan = 1 }, + new JoinMetadata { Description = "Clear", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Enter")] + public JoinDataComplete Enter = new JoinDataComplete(new JoinData { JoinNumber = 37, JoinSpan = 1 }, + new JoinMetadata { Description = "Enter", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Red")] + public JoinDataComplete Red = new JoinDataComplete(new JoinData { JoinNumber = 38, JoinSpan = 1 }, + new JoinMetadata { Description = "Red", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Green")] + public JoinDataComplete Green = new JoinDataComplete(new JoinData { JoinNumber = 39, JoinSpan = 1 }, + new JoinMetadata { Description = "Green", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Yellow")] + public JoinDataComplete Yellow = new JoinDataComplete(new JoinData { JoinNumber = 40, JoinSpan = 1 }, + new JoinMetadata { Description = "Yellow", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Blue")] + public JoinDataComplete Blue = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, + new JoinMetadata { Description = "Blue", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom1")] + public JoinDataComplete Custom1 = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom1", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom2")] + public JoinDataComplete Custom2 = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom2", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom3")] + public JoinDataComplete Custom3 = new JoinDataComplete(new JoinData { JoinNumber = 44, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom3", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom4")] + public JoinDataComplete Custom4 = new JoinDataComplete(new JoinData { JoinNumber = 45, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom4", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom5")] + public JoinDataComplete Custom5 = new JoinDataComplete(new JoinData { JoinNumber = 46, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom5", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom6")] + public JoinDataComplete Custom6 = new JoinDataComplete(new JoinData { JoinNumber = 47, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom6", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom7")] + public JoinDataComplete Custom7 = new JoinDataComplete(new JoinData { JoinNumber = 48, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom7", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom8")] + public JoinDataComplete Custom8 = new JoinDataComplete(new JoinData { JoinNumber = 49, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom8", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Custom9")] + public JoinDataComplete Custom9 = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1 }, + new JoinMetadata { Description = "Custom9", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Fav")] + public JoinDataComplete Fav = new JoinDataComplete(new JoinData { JoinNumber = 51, JoinSpan = 1 }, + new JoinMetadata { Description = "Fav", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Home")] + public JoinDataComplete Home = new JoinDataComplete(new JoinData { JoinNumber = 52, JoinSpan = 1 }, + new JoinMetadata { Description = "Home", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("BatteryLow")] + public JoinDataComplete BatteryLow = new JoinDataComplete(new JoinData { JoinNumber = 53, JoinSpan = 1 }, + new JoinMetadata { Description = "BatteryLow", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("BatteryCritical")] + public JoinDataComplete BatteryCritical = new JoinDataComplete(new JoinData { JoinNumber = 54, JoinSpan = 1 }, + new JoinMetadata { Description = "BatteryCritical", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("BatteryVoltage")] + public JoinDataComplete BatteryVoltage = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "BatteryVoltage", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + /// - /// Represents a Hrxxx0WirelessRemoteControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class Hrxxx0WirelessRemoteControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public Hrxxx0WirelessRemoteControllerJoinMap(uint joinStart) + : this(joinStart, typeof(Hrxxx0WirelessRemoteControllerJoinMap)) { - /// - /// Power - /// - [JoinName("Power")] - public JoinDataComplete Power = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Power", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + } - /// - /// Menu - /// - [JoinName("Menu")] - public JoinDataComplete Menu = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Menu", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Guide - /// - [JoinName("Guide")] - public JoinDataComplete Guide = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Guide", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Info - /// - [JoinName("Info")] - public JoinDataComplete Info = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Info", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// VolumeUp - /// - [JoinName("VolumeUp")] - public JoinDataComplete VolumeUp = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "VolumeUp", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// VolumeDown - /// - [JoinName("VolumeDown")] - public JoinDataComplete VolumeDown = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "VolumeDown", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DialPadUp - /// - [JoinName("DialPadUp")] - public JoinDataComplete DialPadUp = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "DialPadUp", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DialPadDown - /// - [JoinName("DialPadDown")] - public JoinDataComplete DialPadDown = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "DialPadDown", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DialPadLeft - /// - [JoinName("DialPadLeft")] - public JoinDataComplete DialPadLeft = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, - new JoinMetadata { Description = "DialPadLeft", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DialPadRight - /// - [JoinName("DialPadRight")] - public JoinDataComplete DialPadRight = new JoinDataComplete(new JoinData { JoinNumber = 10, JoinSpan = 1 }, - new JoinMetadata { Description = "DialPadRight", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// DialPadSelect - /// - [JoinName("DialPadSelect")] - public JoinDataComplete DialPadSelect = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "DialPadSelect", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// ChannelUp - /// - [JoinName("ChannelUp")] - public JoinDataComplete ChannelUp = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "ChannelUp", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// ChannelDown - /// - [JoinName("ChannelDown")] - public JoinDataComplete ChannelDown = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "ChannelDown", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Mute - /// - [JoinName("Mute")] - public JoinDataComplete Mute = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "Mute", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Exit - /// - [JoinName("Exit")] - public JoinDataComplete Exit = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "Exit", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Last - /// - [JoinName("Last")] - public JoinDataComplete Last = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, - new JoinMetadata { Description = "Last", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Play - /// - [JoinName("Play")] - public JoinDataComplete Play = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, - new JoinMetadata { Description = "Play", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Pause - /// - [JoinName("Pause")] - public JoinDataComplete Pause = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, - new JoinMetadata { Description = "Pause", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Rewind - /// - [JoinName("Rewind")] - public JoinDataComplete Rewind = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, - new JoinMetadata { Description = "Rewind", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// FastForward - /// - [JoinName("FastForward")] - public JoinDataComplete FastForward = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, - new JoinMetadata { Description = "FastForward", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// PreviousTrack - /// - [JoinName("PreviousTrack")] - public JoinDataComplete PreviousTrack = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, - new JoinMetadata { Description = "PreviousTrack", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// NextTrack - /// - [JoinName("NextTrack")] - public JoinDataComplete NextTrack = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, - new JoinMetadata { Description = "NextTrack", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Stop - /// - [JoinName("Stop")] - public JoinDataComplete Stop = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, - new JoinMetadata { Description = "Stop", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Record - /// - [JoinName("Record")] - public JoinDataComplete Record = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, - new JoinMetadata { Description = "Record", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Dvr - /// - [JoinName("Dvr")] - public JoinDataComplete Dvr = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, - new JoinMetadata { Description = "Dvr", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad1 - /// - [JoinName("Keypad1")] - public JoinDataComplete Keypad1 = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad1", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad2Abc - /// - [JoinName("Keypad2Abc")] - public JoinDataComplete Keypad2 = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad2Abc", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad3Def - /// - [JoinName("Keypad3Def")] - public JoinDataComplete Keypad3Def = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad3Def", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad4Ghi - /// - [JoinName("Keypad4Ghi")] - public JoinDataComplete Keypad4Ghi = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad4Ghi", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad5Jkl - /// - [JoinName("Keypad5Jkl")] - public JoinDataComplete Keypad5Jkl = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad5Jkl", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad6Mno - /// - [JoinName("Keypad6Mno")] - public JoinDataComplete Keypad6Mno = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad6Mno", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad7Pqrs - /// - [JoinName("Keypad7Pqrs")] - public JoinDataComplete Keypad7Pqrs = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad7Pqrs", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad8Tuv - /// - [JoinName("Keypad8Tuv")] - public JoinDataComplete Keypad8Tuv = new JoinDataComplete(new JoinData { JoinNumber = 33, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad8Tuv", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad9Wxyz - /// - [JoinName("Keypad9Wxyz")] - public JoinDataComplete Keypad9Wxyz = new JoinDataComplete(new JoinData { JoinNumber = 34, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad9Wxyz", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad0 - /// - [JoinName("Keypad0")] - public JoinDataComplete Keypad0 = new JoinDataComplete(new JoinData { JoinNumber = 35, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad0", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Clear - /// - [JoinName("Clear")] - public JoinDataComplete Clear = new JoinDataComplete(new JoinData { JoinNumber = 36, JoinSpan = 1 }, - new JoinMetadata { Description = "Clear", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Enter - /// - [JoinName("Enter")] - public JoinDataComplete Enter = new JoinDataComplete(new JoinData { JoinNumber = 37, JoinSpan = 1 }, - new JoinMetadata { Description = "Enter", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Red - /// - [JoinName("Red")] - public JoinDataComplete Red = new JoinDataComplete(new JoinData { JoinNumber = 38, JoinSpan = 1 }, - new JoinMetadata { Description = "Red", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Green - /// - [JoinName("Green")] - public JoinDataComplete Green = new JoinDataComplete(new JoinData { JoinNumber = 39, JoinSpan = 1 }, - new JoinMetadata { Description = "Green", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Yellow - /// - [JoinName("Yellow")] - public JoinDataComplete Yellow = new JoinDataComplete(new JoinData { JoinNumber = 40, JoinSpan = 1 }, - new JoinMetadata { Description = "Yellow", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Blue - /// - [JoinName("Blue")] - public JoinDataComplete Blue = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, - new JoinMetadata { Description = "Blue", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom1 - /// - [JoinName("Custom1")] - public JoinDataComplete Custom1 = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom1", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom2 - /// - [JoinName("Custom2")] - public JoinDataComplete Custom2 = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom2", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom3 - /// - [JoinName("Custom3")] - public JoinDataComplete Custom3 = new JoinDataComplete(new JoinData { JoinNumber = 44, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom3", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom4 - /// - [JoinName("Custom4")] - public JoinDataComplete Custom4 = new JoinDataComplete(new JoinData { JoinNumber = 45, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom4", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom5 - /// - [JoinName("Custom5")] - public JoinDataComplete Custom5 = new JoinDataComplete(new JoinData { JoinNumber = 46, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom5", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom6 - /// - [JoinName("Custom6")] - public JoinDataComplete Custom6 = new JoinDataComplete(new JoinData { JoinNumber = 47, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom6", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom7 - /// - [JoinName("Custom7")] - public JoinDataComplete Custom7 = new JoinDataComplete(new JoinData { JoinNumber = 48, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom7", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom8 - /// - [JoinName("Custom8")] - public JoinDataComplete Custom8 = new JoinDataComplete(new JoinData { JoinNumber = 49, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom8", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Custom9 - /// - [JoinName("Custom9")] - public JoinDataComplete Custom9 = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1 }, - new JoinMetadata { Description = "Custom9", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Fav - /// - [JoinName("Fav")] - public JoinDataComplete Fav = new JoinDataComplete(new JoinData { JoinNumber = 51, JoinSpan = 1 }, - new JoinMetadata { Description = "Fav", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Home - /// - [JoinName("Home")] - public JoinDataComplete Home = new JoinDataComplete(new JoinData { JoinNumber = 52, JoinSpan = 1 }, - new JoinMetadata { Description = "Home", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// BatteryLow - /// - [JoinName("BatteryLow")] - public JoinDataComplete BatteryLow = new JoinDataComplete(new JoinData { JoinNumber = 53, JoinSpan = 1 }, - new JoinMetadata { Description = "BatteryLow", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// BatteryCritical - /// - [JoinName("BatteryCritical")] - public JoinDataComplete BatteryCritical = new JoinDataComplete(new JoinData { JoinNumber = 54, JoinSpan = 1 }, - new JoinMetadata { Description = "BatteryCritical", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// BatteryVoltage - /// - [JoinName("BatteryVoltage")] - public JoinDataComplete BatteryVoltage = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "BatteryVoltage", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public Hrxxx0WirelessRemoteControllerJoinMap(uint joinStart) - : this(joinStart, typeof(Hrxxx0WirelessRemoteControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected Hrxxx0WirelessRemoteControllerJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected Hrxxx0WirelessRemoteControllerJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IAnalogInputJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IAnalogInputJoinMap.cs index 59f90347..80509ecc 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IAnalogInputJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IAnalogInputJoinMap.cs @@ -1,44 +1,33 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class IAnalogInputJoinMap : JoinMapBaseAdvanced { + + [JoinName("InputValue")] + public JoinDataComplete InputValue = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Input Value", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + [JoinName("MinimumChange")] + public JoinDataComplete MinimumChange = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Minimum voltage change required to reflect a change", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + /// - /// Represents a IAnalogInputJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class IAnalogInputJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public IAnalogInputJoinMap(uint joinStart) + : this(joinStart, typeof(IAnalogInputJoinMap)) { + } - /// - /// Input Value - /// - [JoinName("InputValue")] - public JoinDataComplete InputValue = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Input Value", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Minimum Change - /// - [JoinName("MinimumChange")] - public JoinDataComplete MinimumChange = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Minimum voltage change required to reflect a change", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public IAnalogInputJoinMap(uint joinStart) - : this(joinStart, typeof(IAnalogInputJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected IAnalogInputJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected IAnalogInputJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IBasicCommunicationJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IBasicCommunicationJoinMap.cs index b4c473cb..1db5b1ff 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IBasicCommunicationJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IBasicCommunicationJoinMap.cs @@ -1,71 +1,49 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class IBasicCommunicationJoinMap : JoinMapBaseAdvanced { + [JoinName("TextReceived")] + public JoinDataComplete TextReceived = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Text Received From Remote Device", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("SendText")] + public JoinDataComplete SendText = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Text Sent To Remote Device", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("SetPortConfig")] + public JoinDataComplete SetPortConfig = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Set Port Config", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("Connect")] + public JoinDataComplete Connect = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Connect", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Connected")] + public JoinDataComplete Connected = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Connected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Status")] + public JoinDataComplete Status = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + /// - /// Represents a IBasicCommunicationJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class IBasicCommunicationJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public IBasicCommunicationJoinMap(uint joinStart) + : this(joinStart, typeof(IBasicCommunicationJoinMap)) { - /// - /// Text Received From Remote Device - /// - [JoinName("TextReceived")] - public JoinDataComplete TextReceived = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Text Received From Remote Device", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + } - /// - /// Text Sent To Remote Device - /// - [JoinName("SendText")] - public JoinDataComplete SendText = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Text Sent To Remote Device", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Set Port Config - /// - [JoinName("SetPortConfig")] - public JoinDataComplete SetPortConfig = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Set Port Config", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Connect - /// - [JoinName("Connect")] - public JoinDataComplete Connect = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Connect", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Disconnect - /// - [JoinName("Connected")] - public JoinDataComplete Connected = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Connected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Status - /// - [JoinName("Status")] - public JoinDataComplete Status = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public IBasicCommunicationJoinMap(uint joinStart) - : this(joinStart, typeof(IBasicCommunicationJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected IBasicCommunicationJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected IBasicCommunicationJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IDigitalInputJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IDigitalInputJoinMap.cs index 0f77fce3..ceed325d 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IDigitalInputJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IDigitalInputJoinMap.cs @@ -1,36 +1,30 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class IDigitalInputJoinMap : JoinMapBaseAdvanced { + + [JoinName("InputState")] + public JoinDataComplete InputState = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Input State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + /// - /// Represents a IDigitalInputJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class IDigitalInputJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public IDigitalInputJoinMap(uint joinStart) + : this(joinStart, typeof(IDigitalInputJoinMap)) { - /// - /// Input State - /// - [JoinName("InputState")] - public JoinDataComplete InputState = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Input State", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + } - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public IDigitalInputJoinMap(uint joinStart) - : this(joinStart, typeof(IDigitalInputJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected IDigitalInputJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected IDigitalInputJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IDigitalOutputJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IDigitalOutputJoinMap.cs index 05895acb..8abb2b74 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IDigitalOutputJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IDigitalOutputJoinMap.cs @@ -1,36 +1,30 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class IDigitalOutputJoinMap : JoinMapBaseAdvanced { + + [JoinName("OutputState")] + public JoinDataComplete OutputState = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Get / Set state of Digital Input", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + /// - /// Represents a IDigitalOutputJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class IDigitalOutputJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public IDigitalOutputJoinMap(uint joinStart) + : this(joinStart, typeof(IDigitalOutputJoinMap)) { - /// - /// Output State - /// - [JoinName("OutputState")] - public JoinDataComplete OutputState = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Get / Set state of Digital Input", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + } - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public IDigitalOutputJoinMap(uint joinStart) - : this(joinStart, typeof(IDigitalOutputJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected IDigitalOutputJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected IDigitalOutputJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IRBlurayBaseJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IRBlurayBaseJoinMap.cs index 3e66eda6..d63a1f52 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IRBlurayBaseJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/IRBlurayBaseJoinMap.cs @@ -4,359 +4,218 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +/// +/// Join map for IRBlurayBase devices +/// +public class IRBlurayBaseJoinMap : JoinMapBaseAdvanced { - /// - /// Join map for IRBlurayBase devices + [JoinName("PowerOn")] + public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Power On", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PowerOff")] + public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Power Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PowerToggle")] + public JoinDataComplete PowerToggle = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Power Toggle", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Up")] + public JoinDataComplete Up = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Nav Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Down")] + public JoinDataComplete Down = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Nav Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Left")] + public JoinDataComplete Left = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Nav Left", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Right")] + public JoinDataComplete Right = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "Nav Right", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Select")] + public JoinDataComplete Select = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "Select", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Menu")] + public JoinDataComplete Menu = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, + new JoinMetadata { Description = "Menu", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Exit")] + public JoinDataComplete Exit = new JoinDataComplete(new JoinData { JoinNumber = 10, JoinSpan = 1 }, + new JoinMetadata { Description = "Exit", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit0")] + public JoinDataComplete Digit0 = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 0", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit1")] + public JoinDataComplete Digit1 = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 1", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit2")] + public JoinDataComplete Digit2 = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 2", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit3")] + public JoinDataComplete Digit3 = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 3", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit4")] + public JoinDataComplete Digit4 = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 4", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit5")] + public JoinDataComplete Digit5 = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 5", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit6")] + public JoinDataComplete Digit6 = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 6", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit7")] + public JoinDataComplete Digit7 = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 7", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit8")] + public JoinDataComplete Digit8 = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 8", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit9")] + public JoinDataComplete Digit9 = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, + new JoinMetadata { Description = "Digit 9", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadClear")] + public JoinDataComplete KeypadClear = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad Clear", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadEnter")] + public JoinDataComplete KeypadEnter = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad Enter", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChannelUp")] + public JoinDataComplete ChannelUp = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Channel Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChannelDown")] + public JoinDataComplete ChannelDown = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Channel Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("LastChannel")] + public JoinDataComplete LastChannel = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, + new JoinMetadata { Description = "Last Channel", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Guide")] + public JoinDataComplete Guide = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, + new JoinMetadata { Description = "Guide", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Info")] + public JoinDataComplete Info = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, + new JoinMetadata { Description = "Info", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Red")] + public JoinDataComplete Red = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, + new JoinMetadata { Description = "Red", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Green")] + public JoinDataComplete Green = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, + new JoinMetadata { Description = "Green", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Yellow")] + public JoinDataComplete Yellow = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, + new JoinMetadata { Description = "Yellow", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Blue")] + public JoinDataComplete Blue = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, + new JoinMetadata { Description = "Blue", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + + [JoinName("Play")] + public JoinDataComplete Play = new JoinDataComplete(new JoinData { JoinNumber = 33, JoinSpan = 1 }, + new JoinMetadata { Description = "Play", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Pause")] + public JoinDataComplete Pause = new JoinDataComplete(new JoinData { JoinNumber = 34, JoinSpan = 1 }, + new JoinMetadata { Description = "Pause", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Stop")] + public JoinDataComplete Stop = new JoinDataComplete(new JoinData { JoinNumber = 35, JoinSpan = 1 }, + new JoinMetadata { Description = "Stop", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("FFwd")] + public JoinDataComplete FFwd = new JoinDataComplete(new JoinData { JoinNumber = 36, JoinSpan = 1 }, + new JoinMetadata { Description = "FFwd", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Rewind")] + public JoinDataComplete Rewind = new JoinDataComplete(new JoinData { JoinNumber = 37, JoinSpan = 1 }, + new JoinMetadata { Description = "Rewind", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChapPlus")] + public JoinDataComplete ChapPlus = new JoinDataComplete(new JoinData { JoinNumber = 38, JoinSpan = 1 }, + new JoinMetadata { Description = "Chapter Plus", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChapMinus")] + public JoinDataComplete ChapMinus = new JoinDataComplete(new JoinData { JoinNumber = 39, JoinSpan = 1 }, + new JoinMetadata { Description = "Chapter Minus", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Replay")] + public JoinDataComplete Replay = new JoinDataComplete(new JoinData { JoinNumber = 40, JoinSpan = 1 }, + new JoinMetadata { Description = "Replay", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Record")] + public JoinDataComplete Record = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, + new JoinMetadata { Description = "Record", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HasKeypadAccessoryButton1")] + public JoinDataComplete HasKeypadAccessoryButton1 = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, + new JoinMetadata { Description = "Has Keypad Accessory Button 1", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HasKeypadAccessoryButton2")] + public JoinDataComplete HasKeypadAccessoryButton2 = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, + new JoinMetadata { Description = "Has Keypad Accessory Button 2", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadAccessoryButton1Press")] + public JoinDataComplete KeypadAccessoryButton1Press = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 2 }, + new JoinMetadata { Description = "Keypad Accessory Button 1 Press", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadAccessoryButton2Press")] + public JoinDataComplete KeypadAccessoryButton2Press = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 2 }, + new JoinMetadata { Description = "Keypad Accessory Button 2 Press", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadAccessoryButton1Label")] + public JoinDataComplete KeypadAccessoryButton1Label = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad Accessory Button 1 Label", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("KeypadAccessoryButton2Label")] + public JoinDataComplete KeypadAccessoryButton2Label = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, + new JoinMetadata { Description = "Keypad Accessory Button 1 Label", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + /// + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class IRBlurayBaseJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public IRBlurayBaseJoinMap(uint joinStart) + : this(joinStart, typeof(IRBlurayBaseJoinMap)) { - /// - /// Power On - /// - [JoinName("PowerOn")] - public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Power On", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + } - /// - /// Power Off - /// - [JoinName("PowerOff")] - public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Power Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Power Toggle - /// - [JoinName("PowerToggle")] - public JoinDataComplete PowerToggle = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Power Toggle", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Nav Up - /// - [JoinName("Up")] - public JoinDataComplete Up = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Nav Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Nav Down - /// - [JoinName("Down")] - public JoinDataComplete Down = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Nav Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Nav Left - /// - [JoinName("Left")] - public JoinDataComplete Left = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Nav Left", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Nav Right - /// - [JoinName("Right")] - public JoinDataComplete Right = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "Nav Right", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Select - /// - [JoinName("Select")] - public JoinDataComplete Select = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "Select", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Menu - /// - [JoinName("Menu")] - public JoinDataComplete Menu = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, - new JoinMetadata { Description = "Menu", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Exit - /// - [JoinName("Exit")] - public JoinDataComplete Exit = new JoinDataComplete(new JoinData { JoinNumber = 10, JoinSpan = 1 }, - new JoinMetadata { Description = "Exit", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 0 - /// - [JoinName("Digit0")] - public JoinDataComplete Digit0 = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 0", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 1 - /// - [JoinName("Digit1")] - public JoinDataComplete Digit1 = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 1", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 2 - /// - [JoinName("Digit2")] - public JoinDataComplete Digit2 = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 2", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 3 - /// - [JoinName("Digit3")] - public JoinDataComplete Digit3 = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 3", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 4 - /// - [JoinName("Digit4")] - public JoinDataComplete Digit4 = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 4", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 5 - /// - [JoinName("Digit5")] - public JoinDataComplete Digit5 = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 5", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 6 - /// - [JoinName("Digit6")] - public JoinDataComplete Digit6 = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 6", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 7 - /// - [JoinName("Digit7")] - public JoinDataComplete Digit7 = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 7", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 8 - /// - [JoinName("Digit8")] - public JoinDataComplete Digit8 = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 8", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Digit 9 - /// - [JoinName("Digit9")] - public JoinDataComplete Digit9 = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, - new JoinMetadata { Description = "Digit 9", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad Clear - /// - [JoinName("KeypadClear")] - public JoinDataComplete KeypadClear = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad Clear", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad Enter - /// - [JoinName("KeypadEnter")] - public JoinDataComplete KeypadEnter = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad Enter", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Channel Up - /// - [JoinName("ChannelUp")] - public JoinDataComplete ChannelUp = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Channel Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Channel Down - /// - [JoinName("ChannelDown")] - public JoinDataComplete ChannelDown = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Channel Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Last Channel - /// - [JoinName("LastChannel")] - public JoinDataComplete LastChannel = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, - new JoinMetadata { Description = "Last Channel", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Guide - /// - [JoinName("Guide")] - public JoinDataComplete Guide = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, - new JoinMetadata { Description = "Guide", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Info - /// - [JoinName("Info")] - public JoinDataComplete Info = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, - new JoinMetadata { Description = "Info", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Red - /// - [JoinName("Red")] - public JoinDataComplete Red = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, - new JoinMetadata { Description = "Red", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Green - /// - [JoinName("Green")] - public JoinDataComplete Green = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, - new JoinMetadata { Description = "Green", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Yellow - /// - [JoinName("Yellow")] - public JoinDataComplete Yellow = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, - new JoinMetadata { Description = "Yellow", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Blue - /// - [JoinName("Blue")] - public JoinDataComplete Blue = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, - new JoinMetadata { Description = "Blue", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Play - /// - [JoinName("Play")] - public JoinDataComplete Play = new JoinDataComplete(new JoinData { JoinNumber = 33, JoinSpan = 1 }, - new JoinMetadata { Description = "Play", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Pause - /// - [JoinName("Pause")] - public JoinDataComplete Pause = new JoinDataComplete(new JoinData { JoinNumber = 34, JoinSpan = 1 }, - new JoinMetadata { Description = "Pause", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Stop - /// - [JoinName("Stop")] - public JoinDataComplete Stop = new JoinDataComplete(new JoinData { JoinNumber = 35, JoinSpan = 1 }, - new JoinMetadata { Description = "Stop", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Fast Forward - /// - [JoinName("FFwd")] - public JoinDataComplete FFwd = new JoinDataComplete(new JoinData { JoinNumber = 36, JoinSpan = 1 }, - new JoinMetadata { Description = "FFwd", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Rewind - /// - [JoinName("Rewind")] - public JoinDataComplete Rewind = new JoinDataComplete(new JoinData { JoinNumber = 37, JoinSpan = 1 }, - new JoinMetadata { Description = "Rewind", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Chapter Plus - /// - [JoinName("ChapPlus")] - public JoinDataComplete ChapPlus = new JoinDataComplete(new JoinData { JoinNumber = 38, JoinSpan = 1 }, - new JoinMetadata { Description = "Chapter Plus", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Chapter Minus - /// - [JoinName("ChapMinus")] - public JoinDataComplete ChapMinus = new JoinDataComplete(new JoinData { JoinNumber = 39, JoinSpan = 1 }, - new JoinMetadata { Description = "Chapter Minus", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Replay - /// - [JoinName("Replay")] - public JoinDataComplete Replay = new JoinDataComplete(new JoinData { JoinNumber = 40, JoinSpan = 1 }, - new JoinMetadata { Description = "Replay", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Record - /// - [JoinName("Record")] - public JoinDataComplete Record = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, - new JoinMetadata { Description = "Record", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Has Keypad Accessory Button 1 - /// - [JoinName("HasKeypadAccessoryButton1")] - public JoinDataComplete HasKeypadAccessoryButton1 = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, - new JoinMetadata { Description = "Has Keypad Accessory Button 1", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Has Keypad Accessory Button 2 - /// - [JoinName("HasKeypadAccessoryButton2")] - public JoinDataComplete HasKeypadAccessoryButton2 = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, - new JoinMetadata { Description = "Has Keypad Accessory Button 2", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad Accessory Button 1 Press - /// - [JoinName("KeypadAccessoryButton1Press")] - public JoinDataComplete KeypadAccessoryButton1Press = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 2 }, - new JoinMetadata { Description = "Keypad Accessory Button 1 Press", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad Accessory Button 2 Press - /// - [JoinName("KeypadAccessoryButton2Press")] - public JoinDataComplete KeypadAccessoryButton2Press = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 2 }, - new JoinMetadata { Description = "Keypad Accessory Button 2 Press", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Keypad Accessory Button 1 Label - /// - [JoinName("KeypadAccessoryButton1Label")] - public JoinDataComplete KeypadAccessoryButton1Label = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad Accessory Button 1 Label", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Keypad Accessory Button 2 Label - /// - [JoinName("KeypadAccessoryButton2Label")] - public JoinDataComplete KeypadAccessoryButton2Label = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, - new JoinMetadata { Description = "Keypad Accessory Button 1 Label", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public IRBlurayBaseJoinMap(uint joinStart) - : this(joinStart, typeof(IRBlurayBaseJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected IRBlurayBaseJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected IRBlurayBaseJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/PduJoinMapBase.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/PduJoinMapBase.cs index f52b96c4..10bf590d 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/PduJoinMapBase.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/PduJoinMapBase.cs @@ -1,87 +1,59 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class PduJoinMapBase : JoinMapBaseAdvanced { + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "PDU Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("Online")] + public JoinDataComplete Online = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutletCount")] + public JoinDataComplete OutletCount = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Number of COntrolled Outlets", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("OutletName")] + public JoinDataComplete OutletName = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Outlet Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("OutletEnabled")] + public JoinDataComplete OutletEnabled = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Outlet Enabled", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutletPowerCycle")] + public JoinDataComplete OutletPowerCycle = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "Outlet Power Cycle", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutletPowerOn")] + public JoinDataComplete OutletPowerOn = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "Outlet Power On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("OutletPowerOff")] + public JoinDataComplete OutletPowerOff = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "Outlet Power Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + + /// - /// Represents a PduJoinMapBase + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class PduJoinMapBase : JoinMapBaseAdvanced - { - /// - /// PDU Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "PDU Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + /// Join this join map will start at + public PduJoinMapBase(uint joinStart) + :base(joinStart, typeof(PduJoinMapBase)) + { + } - /// - /// PDU Online Status - /// - [JoinName("Online")] - public JoinDataComplete Online = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Number of Controlled Outlets - /// - [JoinName("OutletCount")] - public JoinDataComplete OutletCount = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Number of COntrolled Outlets", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Outlet Name - /// - [JoinName("OutletName")] - public JoinDataComplete OutletName = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Outlet Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Outlet Enabled Status - /// - [JoinName("OutletEnabled")] - public JoinDataComplete OutletEnabled = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Outlet Enabled", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Outlet Power State - /// - [JoinName("OutletPowerCycle")] - public JoinDataComplete OutletPowerCycle = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "Outlet Power Cycle", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Outlet Power On - /// - [JoinName("OutletPowerOn")] - public JoinDataComplete OutletPowerOn = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "Outlet Power On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Outlet Power Off - /// - [JoinName("OutletPowerOff")] - public JoinDataComplete OutletPowerOff = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "Outlet Power Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public PduJoinMapBase(uint joinStart) - :base(joinStart, typeof(PduJoinMapBase)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - public PduJoinMapBase(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + public PduJoinMapBase(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/SetTopBoxControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/SetTopBoxControllerJoinMap.cs index ce9a4290..0705eb93 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/SetTopBoxControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/SetTopBoxControllerJoinMap.cs @@ -1,402 +1,239 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class SetTopBoxControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("PowerOn")] + public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Power On", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PowerOff")] + public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Power Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("PowerToggle")] + public JoinDataComplete PowerToggle = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Power Toggle", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HasDpad")] + public JoinDataComplete HasDpad = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Has DPad", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Up")] + public JoinDataComplete Up = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Nav Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Down")] + public JoinDataComplete Down = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Nav Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Left")] + public JoinDataComplete Left = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Nav Left", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Right")] + public JoinDataComplete Right = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Nav Right", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Select")] + public JoinDataComplete Select = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Select", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Menu")] + public JoinDataComplete Menu = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Menu", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Exit")] + public JoinDataComplete Exit = new JoinDataComplete(new JoinData { JoinNumber = 10, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Exit", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HasNumeric")] + public JoinDataComplete HasNumeric = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Has Numeric", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit0")] + public JoinDataComplete Digit0 = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 0", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit1")] + public JoinDataComplete Digit1 = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 1", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit2")] + public JoinDataComplete Digit2 = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 2", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit3")] + public JoinDataComplete Digit3 = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 3", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit4")] + public JoinDataComplete Digit4 = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 4", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit5")] + public JoinDataComplete Digit5 = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 5", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit6")] + public JoinDataComplete Digit6 = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 6", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit7")] + public JoinDataComplete Digit7 = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 7", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit8")] + public JoinDataComplete Digit8 = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 8", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Digit9")] + public JoinDataComplete Digit9 = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Digit 9", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Dash")] + public JoinDataComplete Dash = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Dash", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadEnter")] + public JoinDataComplete KeypadEnter = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Keypad Enter", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChannelUp")] + public JoinDataComplete ChannelUp = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Channel Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChannelDown")] + public JoinDataComplete ChannelDown = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Channel Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("LastChannel")] + public JoinDataComplete LastChannel = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Last Channel", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Guide")] + public JoinDataComplete Guide = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Guide", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Info")] + public JoinDataComplete Info = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Info", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Red")] + public JoinDataComplete Red = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Red", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Green")] + public JoinDataComplete Green = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Green", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Yellow")] + public JoinDataComplete Yellow = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Yellow", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Blue")] + public JoinDataComplete Blue = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Blue", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HasDvr")] + public JoinDataComplete HasDvr = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Has DVR", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("DvrList")] + public JoinDataComplete DvrList = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, + new JoinMetadata { Description = "STB DvrList", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Play")] + public JoinDataComplete Play = new JoinDataComplete(new JoinData { JoinNumber = 33, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Play", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Pause")] + public JoinDataComplete Pause = new JoinDataComplete(new JoinData { JoinNumber = 34, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Pause", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Stop")] + public JoinDataComplete Stop = new JoinDataComplete(new JoinData { JoinNumber = 35, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Stop", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("FFwd")] + public JoinDataComplete FFwd = new JoinDataComplete(new JoinData { JoinNumber = 36, JoinSpan = 1 }, + new JoinMetadata { Description = "STB FFwd", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Rewind")] + public JoinDataComplete Rewind = new JoinDataComplete(new JoinData { JoinNumber = 37, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Rewind", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChapPlus")] + public JoinDataComplete ChapPlus = new JoinDataComplete(new JoinData { JoinNumber = 38, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Chapter Plus", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("ChapMinus")] + public JoinDataComplete ChapMinus = new JoinDataComplete(new JoinData { JoinNumber = 39, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Chapter Minus", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Replay")] + public JoinDataComplete Replay = new JoinDataComplete(new JoinData { JoinNumber = 40, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Replay", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Record")] + public JoinDataComplete Record = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Record", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HasKeypadAccessoryButton1")] + public JoinDataComplete HasKeypadAccessoryButton1 = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Has Keypad Accessory Button 1", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HasKeypadAccessoryButton2")] + public JoinDataComplete HasKeypadAccessoryButton2 = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Has Keypad Accessory Button 2", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadAccessoryButton1Press")] + public JoinDataComplete KeypadAccessoryButton1Press = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 2 }, + new JoinMetadata { Description = "STB Keypad Accessory Button 1 Press", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadAccessoryButton2Press")] + public JoinDataComplete KeypadAccessoryButton2Press = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 2 }, + new JoinMetadata { Description = "STB Keypad Accessory Button 2 Press", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("KeypadAccessoryButton1Label")] + public JoinDataComplete KeypadAccessoryButton1Label = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Keypad Accessory Button 1 Label", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("KeypadAccessoryButton2Label")] + public JoinDataComplete KeypadAccessoryButton2Label = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Keypad Accessory Button 1 Label", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("LoadPresets")] + public JoinDataComplete LoadPresets = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Load Presets", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("HasPresets")] + public JoinDataComplete HasPresets = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1 }, + new JoinMetadata { Description = "STB Load Presets", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + /// - /// Represents a SetTopBoxControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class SetTopBoxControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public SetTopBoxControllerJoinMap(uint joinStart) + : this(joinStart, typeof(SetTopBoxControllerJoinMap)) { - /// - /// STB Power On - /// - [JoinName("PowerOn")] - public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Power On", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); + } - /// - /// STB Power Off - /// - [JoinName("PowerOff")] - public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Power Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Power Toggle - /// - [JoinName("PowerToggle")] - public JoinDataComplete PowerToggle = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Power Toggle", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Has DPad - /// - [JoinName("HasDpad")] - public JoinDataComplete HasDpad = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Has DPad", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Nav Up - /// - [JoinName("Up")] - public JoinDataComplete Up = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Nav Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Nav Down - /// - [JoinName("Down")] - public JoinDataComplete Down = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Nav Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Nav Left - /// - [JoinName("Left")] - public JoinDataComplete Left = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Nav Left", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Nav Right - /// - [JoinName("Right")] - public JoinDataComplete Right = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Nav Right", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Select - /// - [JoinName("Select")] - public JoinDataComplete Select = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Select", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Menu - /// - [JoinName("Menu")] - public JoinDataComplete Menu = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Menu", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Exit - /// - [JoinName("Exit")] - public JoinDataComplete Exit = new JoinDataComplete(new JoinData { JoinNumber = 10, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Exit", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Has Numeric - /// - [JoinName("HasNumeric")] - public JoinDataComplete HasNumeric = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Has Numeric", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 0 - /// - [JoinName("Digit0")] - public JoinDataComplete Digit0 = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 0", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 1 - /// - [JoinName("Digit1")] - public JoinDataComplete Digit1 = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 1", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 2 - /// - [JoinName("Digit2")] - public JoinDataComplete Digit2 = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 2", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 3 - /// - [JoinName("Digit3")] - public JoinDataComplete Digit3 = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 3", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 4 - /// - [JoinName("Digit4")] - public JoinDataComplete Digit4 = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 4", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 5 - /// - [JoinName("Digit5")] - public JoinDataComplete Digit5 = new JoinDataComplete(new JoinData { JoinNumber = 16, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 5", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 6 - /// - [JoinName("Digit6")] - public JoinDataComplete Digit6 = new JoinDataComplete(new JoinData { JoinNumber = 17, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 6", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 7 - /// - [JoinName("Digit7")] - public JoinDataComplete Digit7 = new JoinDataComplete(new JoinData { JoinNumber = 18, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 7", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 8 - /// - [JoinName("Digit8")] - public JoinDataComplete Digit8 = new JoinDataComplete(new JoinData { JoinNumber = 19, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 8", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Digit 9 - /// - [JoinName("Digit9")] - public JoinDataComplete Digit9 = new JoinDataComplete(new JoinData { JoinNumber = 20, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Digit 9", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Dash - /// - [JoinName("Dash")] - public JoinDataComplete Dash = new JoinDataComplete(new JoinData { JoinNumber = 21, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Dash", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Keypad Enter - /// - [JoinName("KeypadEnter")] - public JoinDataComplete KeypadEnter = new JoinDataComplete(new JoinData { JoinNumber = 22, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Keypad Enter", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Channel Up - /// - [JoinName("ChannelUp")] - public JoinDataComplete ChannelUp = new JoinDataComplete(new JoinData { JoinNumber = 23, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Channel Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Channel Down - /// - [JoinName("ChannelDown")] - public JoinDataComplete ChannelDown = new JoinDataComplete(new JoinData { JoinNumber = 24, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Channel Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Last Channel - /// - [JoinName("LastChannel")] - public JoinDataComplete LastChannel = new JoinDataComplete(new JoinData { JoinNumber = 25, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Last Channel", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Guide - /// - [JoinName("Guide")] - public JoinDataComplete Guide = new JoinDataComplete(new JoinData { JoinNumber = 26, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Guide", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Info - /// - [JoinName("Info")] - public JoinDataComplete Info = new JoinDataComplete(new JoinData { JoinNumber = 27, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Info", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Red - /// - [JoinName("Red")] - public JoinDataComplete Red = new JoinDataComplete(new JoinData { JoinNumber = 28, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Red", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Green - /// - [JoinName("Green")] - public JoinDataComplete Green = new JoinDataComplete(new JoinData { JoinNumber = 29, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Green", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Yellow - /// - [JoinName("Yellow")] - public JoinDataComplete Yellow = new JoinDataComplete(new JoinData { JoinNumber = 30, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Yellow", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Blue - /// - [JoinName("Blue")] - public JoinDataComplete Blue = new JoinDataComplete(new JoinData { JoinNumber = 31, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Blue", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Has DVR - /// - [JoinName("HasDvr")] - public JoinDataComplete HasDvr = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Has DVR", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Dvr List - /// - [JoinName("DvrList")] - public JoinDataComplete DvrList = new JoinDataComplete(new JoinData { JoinNumber = 32, JoinSpan = 1 }, - new JoinMetadata { Description = "STB DvrList", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Play - /// - [JoinName("Play")] - public JoinDataComplete Play = new JoinDataComplete(new JoinData { JoinNumber = 33, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Play", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Pause - /// - [JoinName("Pause")] - public JoinDataComplete Pause = new JoinDataComplete(new JoinData { JoinNumber = 34, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Pause", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Stop - /// - [JoinName("Stop")] - public JoinDataComplete Stop = new JoinDataComplete(new JoinData { JoinNumber = 35, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Stop", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB FFwd - /// - [JoinName("FFwd")] - public JoinDataComplete FFwd = new JoinDataComplete(new JoinData { JoinNumber = 36, JoinSpan = 1 }, - new JoinMetadata { Description = "STB FFwd", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Rewind - /// - [JoinName("Rewind")] - public JoinDataComplete Rewind = new JoinDataComplete(new JoinData { JoinNumber = 37, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Rewind", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Chapter Plus - /// - [JoinName("ChapPlus")] - public JoinDataComplete ChapPlus = new JoinDataComplete(new JoinData { JoinNumber = 38, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Chapter Plus", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Chapter Minus - /// - [JoinName("ChapMinus")] - public JoinDataComplete ChapMinus = new JoinDataComplete(new JoinData { JoinNumber = 39, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Chapter Minus", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Replay - /// - [JoinName("Replay")] - public JoinDataComplete Replay = new JoinDataComplete(new JoinData { JoinNumber = 40, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Replay", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Record - /// - [JoinName("Record")] - public JoinDataComplete Record = new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Record", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Has Keypad Accessory Button 1 - /// - [JoinName("HasKeypadAccessoryButton1")] - public JoinDataComplete HasKeypadAccessoryButton1 = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Has Keypad Accessory Button 1", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Has Keypad Accessory Button 2 - /// - [JoinName("HasKeypadAccessoryButton2")] - public JoinDataComplete HasKeypadAccessoryButton2 = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Has Keypad Accessory Button 2", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Keypad Accessory Button 1 Press - /// - [JoinName("KeypadAccessoryButton1Press")] - public JoinDataComplete KeypadAccessoryButton1Press = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 2 }, - new JoinMetadata { Description = "STB Keypad Accessory Button 1 Press", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Keypad Accessory Button 2 Press - /// - [JoinName("KeypadAccessoryButton2Press")] - public JoinDataComplete KeypadAccessoryButton2Press = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 2 }, - new JoinMetadata { Description = "STB Keypad Accessory Button 2 Press", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Keypad Accessory Button 1 Label - /// - [JoinName("KeypadAccessoryButton1Label")] - public JoinDataComplete KeypadAccessoryButton1Label = new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Keypad Accessory Button 1 Label", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// STB Keypad Accessory Button 2 Label - /// - [JoinName("KeypadAccessoryButton2Label")] - public JoinDataComplete KeypadAccessoryButton2Label = new JoinDataComplete(new JoinData { JoinNumber = 43, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Keypad Accessory Button 1 Label", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// STB Load Presets - /// - [JoinName("LoadPresets")] - public JoinDataComplete LoadPresets = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Load Presets", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// STB Has Presets - /// - [JoinName("HasPresets")] - public JoinDataComplete HasPresets = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1 }, - new JoinMetadata { Description = "STB Load Presets", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public SetTopBoxControllerJoinMap(uint joinStart) - : this(joinStart, typeof(SetTopBoxControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected SetTopBoxControllerJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected SetTopBoxControllerJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/StatusSignControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/StatusSignControllerJoinMap.cs index 79ada2c8..98cd9218 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/StatusSignControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/StatusSignControllerJoinMap.cs @@ -1,86 +1,58 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class StatusSignControllerJoinMap : JoinMapBaseAdvanced { + [JoinName("IsOnline")] + public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Status Sign Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("Name")] + public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Status Sign Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + + [JoinName("RedControl")] + public JoinDataComplete RedControl = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Status Red LED Enable / Disable", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("RedLed")] + public JoinDataComplete RedLed = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Status Red LED Intensity", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("GreenControl")] + public JoinDataComplete GreenControl = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Status Green LED Enable / Disable", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("GreenLed")] + public JoinDataComplete GreenLed = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Status Green LED Intensity", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + + [JoinName("BlueControl")] + public JoinDataComplete BlueControl = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Status Blue LED Enable / Disable", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + + [JoinName("BlueLed")] + public JoinDataComplete BlueLed = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Status Blue LED Intensity", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); + /// - /// Represents a StatusSignControllerJoinMap + /// Constructor to use when instantiating this Join Map without inheriting from it /// - public class StatusSignControllerJoinMap : JoinMapBaseAdvanced + /// Join this join map will start at + public StatusSignControllerJoinMap(uint joinStart) + : this(joinStart, typeof(StatusSignControllerJoinMap)) { - /// - /// Status Sign Online - /// - [JoinName("IsOnline")] - public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Status Sign Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Status Sign Name - /// - [JoinName("Name")] - public JoinDataComplete Name = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Status Sign Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - - /// - /// Red LED Control - /// - [JoinName("RedControl")] - public JoinDataComplete RedControl = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Status Red LED Enable / Disable", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Red LED Intensity - /// - [JoinName("RedLed")] - public JoinDataComplete RedLed = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Status Red LED Intensity", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Green LED Control - /// - [JoinName("GreenControl")] - public JoinDataComplete GreenControl = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Status Green LED Enable / Disable", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Green LED Intensity - /// - [JoinName("GreenLed")] - public JoinDataComplete GreenLed = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Status Green LED Intensity", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Blue LED Control - /// - [JoinName("BlueControl")] - public JoinDataComplete BlueControl = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Status Blue LED Enable / Disable", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - - /// - /// Blue LED Intensity - /// - [JoinName("BlueLed")] - public JoinDataComplete BlueLed = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Status Blue LED Intensity", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); - - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public StatusSignControllerJoinMap(uint joinStart) - : this(joinStart, typeof(StatusSignControllerJoinMap)) - { - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected StatusSignControllerJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } - } + + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected StatusSignControllerJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { + } + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/SystemMonitorJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/SystemMonitorJoinMap.cs index c53c9fe6..3d9ac81b 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/SystemMonitorJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/SystemMonitorJoinMap.cs @@ -1,235 +1,136 @@ using System; -namespace PepperDash.Essentials.Core.Bridges +namespace PepperDash.Essentials.Core.Bridges; + +public class SystemMonitorJoinMap : JoinMapBaseAdvanced { - /// - /// Represents a SystemMonitorJoinMap - /// - public class SystemMonitorJoinMap : JoinMapBaseAdvanced - { - /// - /// Processor Timezone - /// - [JoinName("TimeZone")] - public JoinDataComplete TimeZone = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Timezone", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); + [JoinName("TimeZone")] + public JoinDataComplete TimeZone = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Timezone", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); - /// - /// Processor Timezone Name - /// - [JoinName("TimeZoneName")] - public JoinDataComplete TimeZoneName = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Timezone Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("TimeZoneName")] + public JoinDataComplete TimeZoneName = new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Timezone Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor OS Version - /// - [JoinName("IOControllerVersion")] - public JoinDataComplete IOControllerVersion = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor IO Controller Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("IOControllerVersion")] + public JoinDataComplete IOControllerVersion = new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor IO Controller Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor SNMP App Version - /// - [JoinName("SnmpAppVersion")] - public JoinDataComplete SnmpAppVersion = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor SNMP App Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("SnmpAppVersion")] + public JoinDataComplete SnmpAppVersion = new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor SNMP App Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor BACNet App Version - /// - [JoinName("BACnetAppVersion")] - public JoinDataComplete BACnetAppVersion = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor BACNet App Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("BACnetAppVersion")] + public JoinDataComplete BACnetAppVersion = new JoinDataComplete(new JoinData { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor BACNet App Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Controller Version - /// - [JoinName("ControllerVersion")] - public JoinDataComplete ControllerVersion = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Controller Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("ControllerVersion")] + public JoinDataComplete ControllerVersion = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Controller Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Serial Number - /// - [JoinName("SerialNumber")] - public JoinDataComplete SerialNumber = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Serial Number", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("SerialNumber")] + public JoinDataComplete SerialNumber = new JoinDataComplete(new JoinData { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Serial Number", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Model - /// - [JoinName("Model")] - public JoinDataComplete Model = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Model", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("Model")] + public JoinDataComplete Model = new JoinDataComplete(new JoinData { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Model", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Uptime - /// - [JoinName("Uptime")] - public JoinDataComplete Uptime = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Uptime", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("Uptime")] + public JoinDataComplete Uptime = new JoinDataComplete(new JoinData { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Uptime", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Last Boot Time - /// - [JoinName("LastBoot")] - public JoinDataComplete LastBoot = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Last Boot", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("LastBoot")] + public JoinDataComplete LastBoot = new JoinDataComplete(new JoinData { JoinNumber = 9, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Last Boot", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Program Offset Join - /// - [JoinName("ProgramOffsetJoin")] - public JoinDataComplete ProgramOffsetJoin = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 5 }, - new JoinMetadata { Description = "All Program Data is offset between slots by 5 - First Joins Start at 11", JoinCapabilities = eJoinCapabilities.None, JoinType = eJoinType.None }); + [JoinName("ProgramOffsetJoin")] + public JoinDataComplete ProgramOffsetJoin = new JoinDataComplete(new JoinData { JoinNumber = 5, JoinSpan = 5 }, + new JoinMetadata { Description = "All Program Data is offset between slots by 5 - First Joins Start at 11", JoinCapabilities = eJoinCapabilities.None, JoinType = eJoinType.None }); - /// - /// Processor Program Start - /// - [JoinName("ProgramStart")] - public JoinDataComplete ProgramStart = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program Start / Fb", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("ProgramStart")] + public JoinDataComplete ProgramStart = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program Start / Fb", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - /// - /// Processor Program Stop - /// - [JoinName("ProgramStop")] - public JoinDataComplete ProgramStop = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program Stop / Fb", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("ProgramStop")] + public JoinDataComplete ProgramStop = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program Stop / Fb", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - /// - /// Processor Program Register - /// - [JoinName("ProgramRegister")] - public JoinDataComplete ProgramRegister = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program Register / Fb", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("ProgramRegister")] + public JoinDataComplete ProgramRegister = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program Register / Fb", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - /// - /// Processor Program Unregister - /// - [JoinName("ProgramUnregister")] - public JoinDataComplete ProgramUnregister = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program UnRegister / Fb", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); + [JoinName("ProgramUnregister")] + public JoinDataComplete ProgramUnregister = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program UnRegister / Fb", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital }); - /// - /// Processor Program Name - /// - [JoinName("ProgramName")] - public JoinDataComplete ProgramName = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("ProgramName")] + public JoinDataComplete ProgramName = new JoinDataComplete(new JoinData { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Program Version - /// - [JoinName("ProgramCompiledTime")] - public JoinDataComplete ProgramCompiledTime = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program Compile Time", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("ProgramCompiledTime")] + public JoinDataComplete ProgramCompiledTime = new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program Compile Time", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Program Crestron Database Version - /// - [JoinName("ProgramCrestronDatabaseVersion")] - public JoinDataComplete ProgramCrestronDatabaseVersion = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program Database Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("ProgramCrestronDatabaseVersion")] + public JoinDataComplete ProgramCrestronDatabaseVersion = new JoinDataComplete(new JoinData { JoinNumber = 13, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program Database Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Program Environment Version - /// - [JoinName("ProgramEnvironmentVersion")] - public JoinDataComplete ProgramEnvironmentVersion = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program Environment Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("ProgramEnvironmentVersion")] + public JoinDataComplete ProgramEnvironmentVersion = new JoinDataComplete(new JoinData { JoinNumber = 14, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program Environment Version", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Program Aggregate Info - /// - [JoinName("AggregatedProgramInfo")] - public JoinDataComplete AggregatedProgramInfo = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Program Aggregate Info Json", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("AggregatedProgramInfo")] + public JoinDataComplete AggregatedProgramInfo = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Program Aggregate Info Json", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Ethernet Offset Join - /// - [JoinName("EthernetOffsetJoin")] - public JoinDataComplete EthernetOffsetJoin = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, - new JoinMetadata { Description = "All Ethernet Data is offset between Nics by 5 - First Joins Start at 76", JoinCapabilities = eJoinCapabilities.None, JoinType = eJoinType.None }); + [JoinName("EthernetOffsetJoin")] + public JoinDataComplete EthernetOffsetJoin = new JoinDataComplete(new JoinData { JoinNumber = 15, JoinSpan = 1 }, + new JoinMetadata { Description = "All Ethernet Data is offset between Nics by 5 - First Joins Start at 76", JoinCapabilities = eJoinCapabilities.None, JoinType = eJoinType.None }); - /// - /// Processor Ethernet Hostname - /// - [JoinName("HostName")] - public JoinDataComplete HostName = new JoinDataComplete(new JoinData { JoinNumber = 76, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Hostname", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("HostName")] + public JoinDataComplete HostName = new JoinDataComplete(new JoinData { JoinNumber = 76, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Hostname", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Current Ip Address - /// - [JoinName("CurrentIpAddress")] - public JoinDataComplete CurrentIpAddress = new JoinDataComplete(new JoinData { JoinNumber = 77, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Current Ip Address", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("CurrentIpAddress")] + public JoinDataComplete CurrentIpAddress = new JoinDataComplete(new JoinData { JoinNumber = 77, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Current Ip Address", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Current Subnet Mask - /// - [JoinName("CurrentSubnetMask")] - public JoinDataComplete CurrentSubnetMask = new JoinDataComplete(new JoinData { JoinNumber = 78, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Current Subnet Mask", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("CurrentSubnetMask")] + public JoinDataComplete CurrentSubnetMask = new JoinDataComplete(new JoinData { JoinNumber = 78, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Current Subnet Mask", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Current Default Gateway - /// - [JoinName("CurrentDefaultGateway")] - public JoinDataComplete CurrentDefaultGateway = new JoinDataComplete(new JoinData { JoinNumber = 79, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Current Default Gateway", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("CurrentDefaultGateway")] + public JoinDataComplete CurrentDefaultGateway = new JoinDataComplete(new JoinData { JoinNumber = 79, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Current Default Gateway", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Static Ip Address - /// - [JoinName("StaticIpAddress")] - public JoinDataComplete StaticIpAddress = new JoinDataComplete(new JoinData { JoinNumber = 80, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Static Ip Address", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("StaticIpAddress")] + public JoinDataComplete StaticIpAddress = new JoinDataComplete(new JoinData { JoinNumber = 80, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Static Ip Address", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Static Subnet Mask - /// - [JoinName("StaticSubnetMask")] - public JoinDataComplete StaticSubnetMask = new JoinDataComplete(new JoinData { JoinNumber = 81, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Static Subnet Mask", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("StaticSubnetMask")] + public JoinDataComplete StaticSubnetMask = new JoinDataComplete(new JoinData { JoinNumber = 81, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Static Subnet Mask", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Static Default Gateway - /// - [JoinName("StaticDefaultGateway")] - public JoinDataComplete StaticDefaultGateway = new JoinDataComplete(new JoinData { JoinNumber = 82, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Static Default Gateway", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("StaticDefaultGateway")] + public JoinDataComplete StaticDefaultGateway = new JoinDataComplete(new JoinData { JoinNumber = 82, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Static Default Gateway", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Domain - /// - [JoinName("Domain")] - public JoinDataComplete Domain = new JoinDataComplete(new JoinData { JoinNumber = 83, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Domain", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("Domain")] + public JoinDataComplete Domain = new JoinDataComplete(new JoinData { JoinNumber = 83, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Domain", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Dns Server - /// - [JoinName("DnsServer")] - public JoinDataComplete DnsServer = new JoinDataComplete(new JoinData { JoinNumber = 84, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Dns Server", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("DnsServer")] + public JoinDataComplete DnsServer = new JoinDataComplete(new JoinData { JoinNumber = 84, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Dns Server", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Mac Address - /// - [JoinName("MacAddress")] - public JoinDataComplete MacAddress = new JoinDataComplete(new JoinData { JoinNumber = 85, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Mac Address", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("MacAddress")] + public JoinDataComplete MacAddress = new JoinDataComplete(new JoinData { JoinNumber = 85, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Mac Address", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); - /// - /// Processor Ethernet Dhcp Status - /// - [JoinName("DhcpStatus")] - public JoinDataComplete DhcpStatus = new JoinDataComplete(new JoinData { JoinNumber = 86, JoinSpan = 1 }, - new JoinMetadata { Description = "Processor Ethernet Dhcp Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); + [JoinName("DhcpStatus")] + public JoinDataComplete DhcpStatus = new JoinDataComplete(new JoinData { JoinNumber = 86, JoinSpan = 1 }, + new JoinMetadata { Description = "Processor Ethernet Dhcp Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); /// /// Processor Reboot @@ -260,23 +161,22 @@ namespace PepperDash.Essentials.Core.Bridges new JoinMetadata { Description = "Resets the program", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); - /// - /// Constructor to use when instantiating this Join Map without inheriting from it - /// - /// Join this join map will start at - public SystemMonitorJoinMap(uint joinStart) - : this(joinStart, typeof(SystemMonitorJoinMap)) - { - } + /// + /// Constructor to use when instantiating this Join Map without inheriting from it + /// + /// Join this join map will start at + public SystemMonitorJoinMap(uint joinStart) + : this(joinStart, typeof(SystemMonitorJoinMap)) + { + } - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - protected SystemMonitorJoinMap(uint joinStart, Type type) - : base(joinStart, type) - { - } + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + protected SystemMonitorJoinMap(uint joinStart, Type type) + : base(joinStart, type) + { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs index 2a7a46c2..b691bbdb 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs @@ -1,10 +1,7 @@ using System; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.Bridges.JoinMaps -{ - /// - /// Represents a VideoCodecControllerJoinMap - /// +namespace PepperDash.Essentials.Core.Bridges.JoinMaps; + public class VideoCodecControllerJoinMap : JoinMapBaseAdvanced { #region Digital @@ -26,27 +23,21 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// If High, will send DTMF tones to the call set by SelectCall analog. If low sends DTMF tones to last connected call. - /// - [JoinName("SendDtmfToSpecificCallIndex")] - public JoinDataComplete SendDtmfToSpecificCallIndex = new JoinDataComplete( - new JoinData - { - JoinNumber = 10, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "If High, will send DTMF tones to the call set by SelectCall analog. If low sends DTMF tones to last connected call.", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("SendDtmfToSpecificCallIndex")] + public JoinDataComplete SendDtmfToSpecificCallIndex = new JoinDataComplete( + new JoinData + { + JoinNumber = 10, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "If High, will send DTMF tones to the call set by SelectCall analog. If low sends DTMF tones to last connected call.", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// DTMF 1 - /// - [JoinName("Dtmf1")] + [JoinName("Dtmf1")] public JoinDataComplete Dtmf1 = new JoinDataComplete( new JoinData { @@ -60,10 +51,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 2 - /// - [JoinName("Dtmf2")] + [JoinName("Dtmf2")] public JoinDataComplete Dtmf2 = new JoinDataComplete( new JoinData { @@ -77,10 +65,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 3 - /// - [JoinName("Dtmf3")] + [JoinName("Dtmf3")] public JoinDataComplete Dtmf3 = new JoinDataComplete( new JoinData { @@ -94,10 +79,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 4 - /// - [JoinName("Dtmf4")] + [JoinName("Dtmf4")] public JoinDataComplete Dtmf4 = new JoinDataComplete( new JoinData { @@ -111,10 +93,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 5 - /// - [JoinName("Dtmf5")] + [JoinName("Dtmf5")] public JoinDataComplete Dtmf5 = new JoinDataComplete( new JoinData { @@ -128,10 +107,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 6 - /// - [JoinName("Dtmf6")] + [JoinName("Dtmf6")] public JoinDataComplete Dtmf6 = new JoinDataComplete( new JoinData { @@ -145,10 +121,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 7 - /// - [JoinName("Dtmf7")] + [JoinName("Dtmf7")] public JoinDataComplete Dtmf7 = new JoinDataComplete( new JoinData { @@ -162,10 +135,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 8 - /// - [JoinName("Dtmf8")] + [JoinName("Dtmf8")] public JoinDataComplete Dtmf8 = new JoinDataComplete( new JoinData { @@ -179,10 +149,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 9 - /// - [JoinName("Dtmf9")] + [JoinName("Dtmf9")] public JoinDataComplete Dtmf9 = new JoinDataComplete( new JoinData { @@ -196,10 +163,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF 0 - /// - [JoinName("Dtmf0")] + [JoinName("Dtmf0")] public JoinDataComplete Dtmf0 = new JoinDataComplete( new JoinData { @@ -213,10 +177,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF * - /// - [JoinName("DtmfStar")] + [JoinName("DtmfStar")] public JoinDataComplete DtmfStar = new JoinDataComplete( new JoinData { @@ -230,10 +191,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// DTMF # - /// - [JoinName("DtmfPound")] + [JoinName("DtmfPound")] public JoinDataComplete DtmfPound = new JoinDataComplete( new JoinData { @@ -247,10 +205,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// End All Calls - /// - [JoinName("EndAllCalls")] + [JoinName("EndAllCalls")] public JoinDataComplete EndAllCalls = new JoinDataComplete( new JoinData { @@ -366,10 +321,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// Dial Phone - /// - [JoinName("DialPhone")] + [JoinName("DialPhone")] public JoinDataComplete DialPhone = new JoinDataComplete( new JoinData { @@ -400,10 +352,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// Hang Up Phone - /// - [JoinName("HangUpPhone")] + [JoinName("HangUpPhone")] public JoinDataComplete HangUpPhone = new JoinDataComplete( new JoinData { @@ -417,56 +366,47 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// End Call - /// - [JoinName("EndCallStart")] - public JoinDataComplete EndCallStart = new JoinDataComplete( - new JoinData - { - JoinNumber = 81, - JoinSpan = 8 - }, - new JoinMetadata - { - Description = "End a specific call by call index. ", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("EndCallStart")] + public JoinDataComplete EndCallStart = new JoinDataComplete( + new JoinData + { + JoinNumber = 81, + JoinSpan = 8 + }, + new JoinMetadata + { + Description = "End a specific call by call index. ", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Join All Calls - /// - [JoinName("JoinAllCalls")] - public JoinDataComplete JoinAllCalls = new JoinDataComplete( - new JoinData - { - JoinNumber = 90, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Join all calls", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("JoinAllCalls")] + public JoinDataComplete JoinAllCalls = new JoinDataComplete( + new JoinData + { + JoinNumber = 90, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Join all calls", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Join Call - /// - [JoinName("JoinCallStart")] - public JoinDataComplete JoinCallStart = new JoinDataComplete( - new JoinData - { - JoinNumber = 91, - JoinSpan = 8 - }, - new JoinMetadata - { - Description = "Join a specific call by call index. ", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("JoinCallStart")] + public JoinDataComplete JoinCallStart = new JoinDataComplete( + new JoinData + { + JoinNumber = 91, + JoinSpan = 8 + }, + new JoinMetadata + { + Description = "Join a specific call by call index. ", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); /// /// Directory Search Busy @@ -604,56 +544,47 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// Directory Disable Auto Dial Selected Line - /// - [JoinName("DirectoryDisableAutoDialSelectedLine")] - public JoinDataComplete DirectoryDisableAutoDialSelectedLine = new JoinDataComplete( - new JoinData - { - JoinNumber = 107, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Set high to disable automatic dialing of a contact when selected", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("DirectoryDisableAutoDialSelectedLine")] + public JoinDataComplete DirectoryDisableAutoDialSelectedLine = new JoinDataComplete( + new JoinData + { + JoinNumber = 107, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Set high to disable automatic dialing of a contact when selected", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Directory Dial Selected Contact Method - /// - [JoinName("DirectoryDialSelectedContactMethod")] - public JoinDataComplete DirectoryDialSelectedContactMethod = new JoinDataComplete( - new JoinData - { - JoinNumber = 108, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Pulse to dial the selected contact method", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("DirectoryDialSelectedContactMethod")] + public JoinDataComplete DirectoryDialSelectedContactMethod = new JoinDataComplete( + new JoinData + { + JoinNumber = 108, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Pulse to dial the selected contact method", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Directory Clear Selected - /// - [JoinName("DirectoryClearSelected")] - public JoinDataComplete DirectoryClearSelected = new JoinDataComplete( - new JoinData - { - JoinNumber = 110, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Clear Selected Entry and String from Search", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("DirectoryClearSelected")] + public JoinDataComplete DirectoryClearSelected = new JoinDataComplete( + new JoinData + { + JoinNumber = 110, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Clear Selected Entry and String from Search", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); /// /// Camera Tilt Up @@ -757,56 +688,47 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// Camera Focus Near - /// - [JoinName("CameraFocusNear")] - public JoinDataComplete CameraFocusNear = new JoinDataComplete( - new JoinData - { - JoinNumber = 117, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Camera Focus Near", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("CameraFocusNear")] + public JoinDataComplete CameraFocusNear = new JoinDataComplete( + new JoinData + { + JoinNumber = 117, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Camera Focus Near", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Camera Focus Far - /// - [JoinName("CameraFocusFar")] - public JoinDataComplete CameraFocusFar = new JoinDataComplete( - new JoinData - { - JoinNumber = 118, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Camera Focus Far", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("CameraFocusFar")] + public JoinDataComplete CameraFocusFar = new JoinDataComplete( + new JoinData + { + JoinNumber = 118, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Camera Focus Far", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Camera Auto Focus - /// - [JoinName("CameraFocusAuto")] - public JoinDataComplete CameraFocusAuto = new JoinDataComplete( - new JoinData - { - JoinNumber = 119, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Camera Auto Focus Trigger", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("CameraFocusAuto")] + public JoinDataComplete CameraFocusAuto = new JoinDataComplete( + new JoinData + { + JoinNumber = 119, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Camera Auto Focus Trigger", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); /// /// Camera Preset Save @@ -961,10 +883,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// Dial Meeting Start - /// - [JoinName("DialMeetingStart")] + [JoinName("DialMeetingStart")] public JoinDataComplete DialMeetingStart = new JoinDataComplete( new JoinData { @@ -1114,39 +1033,33 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// Remove Selected Recent Call Item - /// - [JoinName("RemoveSelectedRecentCallItem")] - public JoinDataComplete RemoveSelectedRecentCallItem = new JoinDataComplete( - new JoinData - { - JoinNumber = 181, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Pulse to remove the selected recent call item specified by the SelectRecentCallItem analog join", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("RemoveSelectedRecentCallItem")] + public JoinDataComplete RemoveSelectedRecentCallItem = new JoinDataComplete( + new JoinData + { + JoinNumber = 181, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Pulse to remove the selected recent call item specified by the SelectRecentCallItem analog join", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Dial Selected Recent Call Item - /// - [JoinName("DialSelectedRecentCallItem")] - public JoinDataComplete DialSelectedRecentCallItem = new JoinDataComplete( - new JoinData - { - JoinNumber = 182, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Pulse to dial the selected recent call item specified by the SelectRecentCallItem analog join", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("DialSelectedRecentCallItem")] + public JoinDataComplete DialSelectedRecentCallItem = new JoinDataComplete( + new JoinData + { + JoinNumber = 182, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Pulse to dial the selected recent call item specified by the SelectRecentCallItem analog join", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); /// /// Source Share Start @@ -1233,141 +1146,117 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Digital }); - /// - /// Hold All Calls - /// - [JoinName("HoldAllCalls")] - public JoinDataComplete HoldAllCalls = new JoinDataComplete( - new JoinData - { - JoinNumber = 220, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Holds all calls", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("HoldAllCalls")] + public JoinDataComplete HoldAllCalls = new JoinDataComplete( + new JoinData + { + JoinNumber = 220, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Holds all calls", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Hold Call at Index - /// - [JoinName("HoldCallsStart")] - public JoinDataComplete HoldCallsStart = new JoinDataComplete( - new JoinData - { - JoinNumber = 221, - JoinSpan = 8 - }, - new JoinMetadata - { - Description = "Holds Call at specified index. FB reported on Call Status XSIG", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("HoldCallsStart")] + public JoinDataComplete HoldCallsStart = new JoinDataComplete( + new JoinData + { + JoinNumber = 221, + JoinSpan = 8 + }, + new JoinMetadata + { + Description = "Holds Call at specified index. FB reported on Call Status XSIG", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Resume All Calls - /// - [JoinName("ResumeCallsStart")] - public JoinDataComplete ResumeCallsStart = new JoinDataComplete( - new JoinData - { - JoinNumber = 231, - JoinSpan = 8 - }, - new JoinMetadata - { - Description = "Resume Call at specified index", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("ResumeCallsStart")] + public JoinDataComplete ResumeCallsStart = new JoinDataComplete( + new JoinData + { + JoinNumber = 231, + JoinSpan = 8 + }, + new JoinMetadata + { + Description = "Resume Call at specified index", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Multi Site Option Is Enabled - /// - [JoinName("MultiSiteOptionIsEnabled")] - public JoinDataComplete MultiSiteOptionIsEnabled = new JoinDataComplete( - new JoinData - { - JoinNumber = 301, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Multi site option is enabled FB", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("MultiSiteOptionIsEnabled")] + public JoinDataComplete MultiSiteOptionIsEnabled = new JoinDataComplete( + new JoinData + { + JoinNumber = 301, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Multi site option is enabled FB", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Auto Answer Enabled - /// - [JoinName("AutoAnswerEnabled")] - public JoinDataComplete AutoAnswerEnabled = new JoinDataComplete( - new JoinData - { - JoinNumber = 302, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Auto Answer is enabled FB", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Digital - }); + [JoinName("AutoAnswerEnabled")] + public JoinDataComplete AutoAnswerEnabled = new JoinDataComplete( + new JoinData + { + JoinNumber = 302, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Auto Answer is enabled FB", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Participant Audio Mute Toggle - /// - [JoinName("ParticipantAudioMuteToggleStart")] - public JoinDataComplete ParticipantAudioMuteToggleStart = new JoinDataComplete( + [JoinName("ParticipantAudioMuteToggleStart")] + public JoinDataComplete ParticipantAudioMuteToggleStart = new JoinDataComplete( new JoinData { JoinNumber = 501, JoinSpan = 50 }, - new JoinMetadata - { - Description = "Toggles the participant's audio mute status", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Digital - }); + new JoinMetadata + { + Description = "Toggles the participant's audio mute status", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Participant Video Mute Toggle - /// - [JoinName("ParticipantVideoMuteToggleStart")] - public JoinDataComplete ParticipantVideoMuteToggleStart = new JoinDataComplete( + [JoinName("ParticipantVideoMuteToggleStart")] + public JoinDataComplete ParticipantVideoMuteToggleStart = new JoinDataComplete( new JoinData { JoinNumber = 801, JoinSpan = 50 }, - new JoinMetadata - { - Description = "Toggles the participant's video mute status", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Digital - }); + new JoinMetadata + { + Description = "Toggles the participant's video mute status", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); - /// - /// Participant Pin Toggle - /// - [JoinName("ParticipantPinToggleStart")] - public JoinDataComplete ParticipantPinToggleStart = new JoinDataComplete( + [JoinName("ParticipantPinToggleStart")] + public JoinDataComplete ParticipantPinToggleStart = new JoinDataComplete( new JoinData { JoinNumber = 1101, JoinSpan = 50 }, - new JoinMetadata - { - Description = "Toggles the participant's pin status", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Digital - }); + new JoinMetadata + { + Description = "Toggles the participant's pin status", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); #endregion @@ -1376,58 +1265,49 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps #region Analog - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + [JoinName("MeetingsToDisplay")] + public JoinDataComplete MeetingsToDisplay = new JoinDataComplete( + new JoinData + { + JoinNumber = 40, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Set/FB the number of meetings to display via the bridge xsig; default: 3 meetings.", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Analog + }); - /// - /// Meetings To Display - /// - [JoinName("MeetingsToDisplay")] - public JoinDataComplete MeetingsToDisplay = new JoinDataComplete( - new JoinData - { - JoinNumber = 40, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Set/FB the number of meetings to display via the bridge xsig; default: 3 meetings.", - JoinCapabilities = eJoinCapabilities.ToFromSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("SelectCall")] + public JoinDataComplete SelectCall = new JoinDataComplete( + new JoinData + { + JoinNumber = 24, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Sets the selected Call for DTMF commands. Valid values 1-8", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Analog + }); - /// - /// Select Call - /// - [JoinName("SelectCall")] - public JoinDataComplete SelectCall = new JoinDataComplete( - new JoinData - { - JoinNumber = 24, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Sets the selected Call for DTMF commands. Valid values 1-8", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Analog - }); - /// - /// Connected Call Count - /// - [JoinName("ConnectedCallCount")] - public JoinDataComplete ConnectedCallCount = new JoinDataComplete( - new JoinData - { - JoinNumber = 25, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Reports the number of currently connected calls", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("ConnectedCallCount")] + public JoinDataComplete ConnectedCallCount = new JoinDataComplete( + new JoinData + { + JoinNumber = 25, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Reports the number of currently connected calls", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Analog + }); /// /// Minutes Before Meeting Start @@ -1463,22 +1343,19 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Analog }); - /// - /// Camera Count - /// - [JoinName("CameraCount")] - public JoinDataComplete CameraCount = new JoinDataComplete( - new JoinData - { - JoinNumber = 61, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Reports the number of cameras", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("CameraCount")] + public JoinDataComplete CameraCount = new JoinDataComplete( + new JoinData + { + JoinNumber = 61, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Reports the number of cameras", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Analog + }); /// /// Directory Row Count @@ -1514,56 +1391,48 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Analog }); - /// - /// Selected Contact Method Count - /// - [JoinName("SelectedContactMethodCount")] - public JoinDataComplete SelectedContactMethodCount = new JoinDataComplete( - new JoinData - { - JoinNumber = 102, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Reports the number of contact methods for the selected contact", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Analog - }); - /// - /// Select Contact Method - /// - [JoinName("SelectContactMethod")] - public JoinDataComplete SelectContactMethod = new JoinDataComplete( - new JoinData - { - JoinNumber = 103, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Selects a contact method by index", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("SelectedContactMethodCount")] + public JoinDataComplete SelectedContactMethodCount = new JoinDataComplete( + new JoinData + { + JoinNumber = 102, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Reports the number of contact methods for the selected contact", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Analog + }); - /// - /// Directory Select Row Feedback - /// - [JoinName("DirectorySelectRowFeedback")] - public JoinDataComplete DirectorySelectRowFeedback = new JoinDataComplete( - new JoinData - { - JoinNumber = 104, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Directory Select Row and Feedback", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("SelectContactMethod")] + public JoinDataComplete SelectContactMethod = new JoinDataComplete( + new JoinData + { + JoinNumber = 103, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Selects a contact method by index", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Analog + }); + + [JoinName("DirectorySelectRowFeedback")] + public JoinDataComplete DirectorySelectRowFeedback = new JoinDataComplete( + new JoinData + { + JoinNumber = 104, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Directory Select Row and Feedback", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Analog + }); /// @@ -1583,22 +1452,19 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Analog }); - /// - /// Far End Preset Select - /// - [JoinName("FarEndPresetSelect")] - public JoinDataComplete FarEndPresetSelect = new JoinDataComplete( - new JoinData - { - JoinNumber = 122, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Far End Preset Preset Select", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("FarEndPresetSelect")] + public JoinDataComplete FarEndPresetSelect = new JoinDataComplete( + new JoinData + { + JoinNumber = 122, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Far End Preset Preset Select", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Analog + }); /// /// Participant Count @@ -1651,56 +1517,47 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Analog }); - /// - /// Select Recent Call Item - /// - [JoinName("SelectRecentCallItem")] - public JoinDataComplete SelectRecentCallItem = new JoinDataComplete( - new JoinData - { - JoinNumber = 180, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Select/FB for Recent Call Item. Valid values 1 - 10", - JoinCapabilities = eJoinCapabilities.ToFromSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("SelectRecentCallItem")] + public JoinDataComplete SelectRecentCallItem = new JoinDataComplete( + new JoinData + { + JoinNumber = 180, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Select/FB for Recent Call Item. Valid values 1 - 10", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Analog + }); - /// - /// Recent Call Occurrence Type - /// - [JoinName("RecentCallOccurrenceType")] - public JoinDataComplete RecentCallOccurrenceType = new JoinDataComplete( - new JoinData - { - JoinNumber = 181, - JoinSpan = 10 - }, - new JoinMetadata - { - Description = "Recent Call Occurrence Type. [0-3] 0 = Unknown, 1 = Placed, 2 = Received, 3 = NoAnswer", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("RecentCallOccurrenceType")] + public JoinDataComplete RecentCallOccurrenceType = new JoinDataComplete( + new JoinData + { + JoinNumber = 181, + JoinSpan = 10 + }, + new JoinMetadata + { + Description = "Recent Call Occurrence Type. [0-3] 0 = Unknown, 1 = Placed, 2 = Received, 3 = NoAnswer", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Analog + }); - /// - /// Recent Call Count - /// - [JoinName("RecentCallCount")] - public JoinDataComplete RecentCallCount = new JoinDataComplete( - new JoinData - { - JoinNumber = 191, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Recent Call Count", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Analog - }); + [JoinName("RecentCallCount")] + public JoinDataComplete RecentCallCount = new JoinDataComplete( + new JoinData + { + JoinNumber = 191, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Recent Call Count", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Analog + }); #endregion @@ -1725,10 +1582,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Serial }); - /// - /// Phone Dial String - /// - [JoinName("PhoneDialString")] + [JoinName("PhoneDialString")] public JoinDataComplete PhoneDialString = new JoinDataComplete( new JoinData { @@ -1742,10 +1596,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Serial }); - /// - /// Current Call Data - /// - [JoinName("CurrentCallData")] + [JoinName("CurrentCallData")] public JoinDataComplete CurrentCallData = new JoinDataComplete( new JoinData { @@ -1861,22 +1712,19 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Serial }); - /// - /// Contact Methods - /// - [JoinName("ContactMethods")] - public JoinDataComplete ContactMethods = new JoinDataComplete( - new JoinData - { - JoinNumber = 103, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Contact Methods - XSig, 10 entries", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("ContactMethods")] + public JoinDataComplete ContactMethods = new JoinDataComplete( + new JoinData + { + JoinNumber = 103, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Contact Methods - XSig, 10 entries", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); /// /// Camera Preset Names @@ -1895,10 +1743,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Serial }); - /// - /// Current Layout String - /// - [JoinName("CurrentLayoutStringFb")] + [JoinName("CurrentLayoutStringFb")] public JoinDataComplete CurrentLayoutStringFb = new JoinDataComplete( new JoinData { @@ -1912,39 +1757,33 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Serial }); - /// - /// Available Layouts XSig - /// - [JoinName("AvailableLayoutsFb")] - public JoinDataComplete AvailableLayoutsFb = new JoinDataComplete( - new JoinData - { - JoinNumber = 142, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "xSig of all available layouts", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("AvailableLayoutsFb")] + public JoinDataComplete AvailableLayoutsFb = new JoinDataComplete( + new JoinData + { + JoinNumber = 142, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "xSig of all available layouts", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// Select Layout - /// - [JoinName("SelectLayout")] - public JoinDataComplete SelectLayout = new JoinDataComplete( - new JoinData - { - JoinNumber = 142, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Select Layout by string", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("SelectLayout")] + public JoinDataComplete SelectLayout = new JoinDataComplete( + new JoinData + { + JoinNumber = 142, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Select Layout by string", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); /// @@ -1964,90 +1803,75 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Serial }); - /// - /// Camera Names XSig - /// - [JoinName("CameraNamesFb")] - public JoinDataComplete CameraNamesFb = new JoinDataComplete( - new JoinData - { - JoinNumber = 161, - JoinSpan = 10 - }, - new JoinMetadata - { - Description = "Camera Name Fb", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("CameraNamesFb")] + public JoinDataComplete CameraNamesFb = new JoinDataComplete( + new JoinData + { + JoinNumber = 161, + JoinSpan = 10 + }, + new JoinMetadata + { + Description = "Camera Name Fb", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// Selected Recent Call Name - /// - [JoinName("SelectedRecentCallName")] - public JoinDataComplete SelectedRecentCallName = new JoinDataComplete( - new JoinData - { - JoinNumber = 171, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Selected Recent Call Name", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("SelectedRecentCallName")] + public JoinDataComplete SelectedRecentCallName = new JoinDataComplete( + new JoinData + { + JoinNumber = 171, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Selected Recent Call Name", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// Selected Recent Call Number - /// - [JoinName("SelectedRecentCallNumber")] - public JoinDataComplete SelectedRecentCallNumber = new JoinDataComplete( - new JoinData - { - JoinNumber = 172, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Selected Recent Call Number", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("SelectedRecentCallNumber")] + public JoinDataComplete SelectedRecentCallNumber = new JoinDataComplete( + new JoinData + { + JoinNumber = 172, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "Selected Recent Call Number", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// Recent Call Names - /// - [JoinName("RecentCallNamesStart")] - public JoinDataComplete RecentCallNamesStart = new JoinDataComplete( - new JoinData - { - JoinNumber = 181, - JoinSpan = 10 - }, - new JoinMetadata - { - Description = "Recent Call Names", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("RecentCallNamesStart")] + public JoinDataComplete RecentCallNamesStart = new JoinDataComplete( + new JoinData + { + JoinNumber = 181, + JoinSpan = 10 + }, + new JoinMetadata + { + Description = "Recent Call Names", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// Recent Call Numbers - /// - [JoinName("RecentCallTimesStart")] - public JoinDataComplete RecentCallTimesStart = new JoinDataComplete( - new JoinData - { - JoinNumber = 191, - JoinSpan = 10 - }, - new JoinMetadata - { - Description = "Recent Calls Times", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("RecentCallTimesStart")] + public JoinDataComplete RecentCallTimesStart = new JoinDataComplete( + new JoinData + { + JoinNumber = 191, + JoinSpan = 10 + }, + new JoinMetadata + { + Description = "Recent Calls Times", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); /// /// Current Source @@ -2083,90 +1907,75 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps JoinType = eJoinType.Serial }); - /// - /// Device IP Address - /// - [JoinName("DeviceIpAddresss")] - public JoinDataComplete DeviceIpAddresss = new JoinDataComplete( - new JoinData - { - JoinNumber = 301, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "IP Address of device", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("DeviceIpAddresss")] + public JoinDataComplete DeviceIpAddresss = new JoinDataComplete( + new JoinData + { + JoinNumber = 301, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "IP Address of device", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// SIP Phone Number - /// - [JoinName("SipPhoneNumber")] - public JoinDataComplete SipPhoneNumber = new JoinDataComplete( - new JoinData - { - JoinNumber = 302, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "SIP phone number of device", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("SipPhoneNumber")] + public JoinDataComplete SipPhoneNumber = new JoinDataComplete( + new JoinData + { + JoinNumber = 302, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "SIP phone number of device", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// E164 Alias - /// - [JoinName("E164Alias")] - public JoinDataComplete E164Alias = new JoinDataComplete( - new JoinData - { - JoinNumber = 303, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "E164 alias of device", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("E164Alias")] + public JoinDataComplete E164Alias = new JoinDataComplete( + new JoinData + { + JoinNumber = 303, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "E164 alias of device", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// H323 ID - /// - [JoinName("H323Id")] - public JoinDataComplete H323Id = new JoinDataComplete( - new JoinData - { - JoinNumber = 304, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "H323 ID of device", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("H323Id")] + public JoinDataComplete H323Id = new JoinDataComplete( + new JoinData + { + JoinNumber = 304, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "H323 ID of device", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); - /// - /// SIP URI - /// - [JoinName("SipUri")] - public JoinDataComplete SipUri = new JoinDataComplete( - new JoinData - { - JoinNumber = 305, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "SIP URI of device", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); + [JoinName("SipUri")] + public JoinDataComplete SipUri = new JoinDataComplete( + new JoinData + { + JoinNumber = 305, + JoinSpan = 1 + }, + new JoinMetadata + { + Description = "SIP URI of device", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); /// /// Selected Directory Entry Name @@ -2241,4 +2050,3 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps { } } -} diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs index 298bbc9b..0c053d6b 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs @@ -10,156 +10,116 @@ using Crestron.SimplSharpPro.DM; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + public class CecPortController : Device, IBasicCommunicationWithStreamDebugging { - /// - /// Represents a CecPortController - /// - public class CecPortController : Device, IBasicCommunicationWithStreamDebugging + public CommunicationStreamDebugging StreamDebugging { get; private set; } + + public event EventHandler BytesReceived; + public event EventHandler TextReceived; + + public bool IsConnected { get { return true; } } + + ICec Port; + + public CecPortController(string key, Func postActivationFunc, + EssentialsControlPropertiesConfig config):base(key) { - /// - /// Gets or sets the StreamDebugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } + StreamDebugging = new CommunicationStreamDebugging(key); - /// - /// Event raised when bytes are received - /// - public event EventHandler BytesReceived; - - /// - /// Event raised when text is received - /// - public event EventHandler TextReceived; - - /// - /// Gets or sets the IsConnected - /// - public bool IsConnected { get { return true; } } - - ICec Port; - - /// - /// Constructor - /// - /// key of the device - /// post activation function for the device - /// configuration for the device - public CecPortController(string key, Func postActivationFunc, - EssentialsControlPropertiesConfig config) : base(key) + AddPostActivationAction(() => { - StreamDebugging = new CommunicationStreamDebugging(key); + Port = postActivationFunc(config); - AddPostActivationAction(() => - { - Port = postActivationFunc(config); + Port.StreamCec.CecChange += StreamCec_CecChange; + }); + } - Port.StreamCec.CecChange += StreamCec_CecChange; - }); + public CecPortController(string key, ICec port) + : base(key) + { + Port = port; + + Port.StreamCec.CecChange += new CecChangeEventHandler(StreamCec_CecChange); + } + + void StreamCec_CecChange(Cec cecDevice, CecEventArgs args) + { + if (args.EventId == CecEventIds.CecMessageReceivedEventId) + OnDataReceived(cecDevice.Received.StringValue); + else if (args.EventId == CecEventIds.ErrorFeedbackEventId) + if(cecDevice.ErrorFeedback.BoolValue) + Debug.LogMessage(LogEventLevel.Verbose, this, "CEC NAK Error"); + } + + void OnDataReceived(string 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)); + } + var textHandler = TextReceived; + if (textHandler != null) + { + if (StreamDebugging.RxStreamDebuggingIsEnabled) + Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", s); + textHandler(this, new GenericCommMethodReceiveTextArgs(s)); + } + } + + #region IBasicCommunication Members + + 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.StreamCec.Send.StringValue = text; + } + + 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)); + Port.StreamCec.Send.StringValue = text; + } + + public void Connect() + { + } + + public void Disconnect() + { + } + + #endregion + + /// + /// + /// + /// + 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); } - /// - /// Constructor - /// - /// key of the device - /// CEC port - public CecPortController(string key, ICec port) - : base(key) - { - Port = port; - - Port.StreamCec.CecChange += new CecChangeEventHandler(StreamCec_CecChange); - } - - void StreamCec_CecChange(Cec cecDevice, CecEventArgs args) - { - if (args.EventId == CecEventIds.CecMessageReceivedEventId) - OnDataReceived(cecDevice.Received.StringValue); - else if (args.EventId == CecEventIds.ErrorFeedbackEventId) - if (cecDevice.ErrorFeedback.BoolValue) - Debug.LogMessage(LogEventLevel.Verbose, this, "CEC NAK Error"); - } - - void OnDataReceived(string s) - { - var bytesHandler = BytesReceived; - if (bytesHandler != null) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(s); - this.PrintReceivedBytes(bytes); - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - } - var textHandler = TextReceived; - if (textHandler != null) - { - this.PrintReceivedText(s); - textHandler(this, new GenericCommMethodReceiveTextArgs(s)); - } - } - - #region IBasicCommunication Members - - /// - /// SendText method - /// - public void SendText(string text) - { - if (Port == null) - return; - this.PrintSentText(text); - Port.StreamCec.Send.StringValue = text; - } - - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) - { - if (Port == null) - return; - var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - this.PrintSentBytes(bytes); - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); - Port.StreamCec.Send.StringValue = text; - } - - /// - /// Connect method - /// - public void Connect() - { - } - - /// - /// 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); - } - - OnDataReceived(b.ToString()); - } + OnDataReceived(b.ToString()); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index ec679fae..a4407e01 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -9,17 +9,11 @@ using PepperDash.Core.Logging; using Serilog.Events; -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a ComPortController - /// +namespace PepperDash.Essentials.Core; + public class ComPortController : Device, IBasicCommunicationWithStreamDebugging { - /// - /// Gets or sets the StreamDebugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } + public CommunicationStreamDebugging StreamDebugging { get; private set; } /// /// Event fired when bytes are received @@ -39,27 +33,20 @@ namespace PepperDash.Essentials.Core ComPort Port; ComPort.ComPortSpec Spec; - /// - /// Constructor - /// - /// - /// - /// - /// - 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; - AddPostActivationAction(() => - { - Port = postActivationFunc(config); + AddPostActivationAction(() => + { + Port = postActivationFunc(config); - RegisterAndConfigureComPort(); - }); - } + RegisterAndConfigureComPort(); + }); + } /// /// Constructor @@ -83,13 +70,22 @@ namespace PepperDash.Essentials.Core RegisterAndConfigureComPort(); } - private void RegisterAndConfigureComPort() - { - if (Port == null) - { - this.LogInformation($"Configured {Port.Parent.GetType().Name}-comport-{Port.ID} for {Key} does not exist."); - return; - } + 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) + { + 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 || Port.Parent is CenIoCom102) @@ -124,31 +120,33 @@ namespace PepperDash.Essentials.Core 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); - this.PrintReceivedBytes(bytes); - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + 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)); eventSubscribed = true; - } - var textHandler = TextReceived; - if (textHandler != null) - { - this.PrintReceivedText(s); - textHandler(this, new GenericCommMethodReceiveTextArgs(s)); + } + var textHandler = TextReceived; + if (textHandler != null) + { + if (StreamDebugging.RxStreamDebuggingIsEnabled) + Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", 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 @@ -169,8 +167,9 @@ namespace PepperDash.Essentials.Core if (Port == null) return; - this.PrintSentText(text); - Port.Send(text); + if (StreamDebugging.TxStreamDebuggingIsEnabled) + Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text); + Port.Send(text); } /// @@ -181,7 +180,8 @@ namespace PepperDash.Essentials.Core if (Port == null) return; var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - this.PrintSentBytes(bytes); + if (StreamDebugging.TxStreamDebuggingIsEnabled) + Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); Port.Send(text); } @@ -202,27 +202,23 @@ namespace PepperDash.Essentials.Core #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); - } + /// + /// + /// + /// + 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 diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComSpecJsonConverter.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComSpecJsonConverter.cs index 11e2bc74..6ef2f916 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComSpecJsonConverter.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComSpecJsonConverter.cs @@ -13,119 +13,89 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// This converter creates a proper ComPort.ComPortSpec struct from more-friendly JSON values. It uses +/// ComSpecPropsJsonConverter to finish the individual properties. +/// +public class ComSpecJsonConverter : JsonConverter { - /// - /// This converter creates a proper ComPort.ComPortSpec struct from more-friendly JSON values. It uses - /// ComSpecPropsJsonConverter to finish the individual properties. - /// - public class ComSpecJsonConverter : JsonConverter + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - /// - /// ReadJson method - /// - /// reader to use - /// type of the object being read - /// existing value of the object being read - /// serializer to use - /// deserialized object - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + if (objectType == typeof(ComPort.ComPortSpec?)) { - if (objectType == typeof(ComPort.ComPortSpec?)) - { - var newSer = new JsonSerializer(); - newSer.Converters.Add(new ComSpecPropsJsonConverter()); - newSer.ObjectCreationHandling = ObjectCreationHandling.Replace; - return newSer.Deserialize(reader); - } - return null; - } - - /// - /// CanConvert method - /// - /// - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ComPort.ComPortSpec?); - } - - /// - /// Gets or sets the CanRead - /// - public override bool CanRead { get { return true; } } - - /// - /// Gets or sets the CanWrite - /// - /// - public override bool CanWrite { get { return false; } } - - /// - /// WriteJson method - /// - /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); + var newSer = new JsonSerializer(); + newSer.Converters.Add(new ComSpecPropsJsonConverter()); + newSer.ObjectCreationHandling = ObjectCreationHandling.Replace; + return newSer.Deserialize(reader); } + return null; } /// - /// Represents a ComSpecPropsJsonConverter + /// /// - public class ComSpecPropsJsonConverter : JsonConverter + public override bool CanConvert(Type objectType) { - /// - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ComPort.eComBaudRates) - || objectType == typeof(ComPort.eComDataBits) - || objectType == typeof(ComPort.eComParityType) - || objectType == typeof(ComPort.eComHardwareHandshakeType) - || objectType == typeof(ComPort.eComSoftwareHandshakeType) - || objectType == typeof(ComPort.eComProtocolType) - || objectType == typeof(ComPort.eComStopBits); - } - - /// - /// Gets or sets the CanRead - /// - /// - public override bool CanRead { get { return true; } } - - /// - /// ReadJson method - /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - //Debug.LogMessage(LogEventLevel.Verbose, "ReadJson type: " + objectType.Name); - if (objectType == typeof(ComPort.eComBaudRates)) - return Enum.Parse(typeof(ComPort.eComBaudRates), "ComspecBaudRate" + reader.Value, false); - else if (objectType == typeof(ComPort.eComDataBits)) - return Enum.Parse(typeof(ComPort.eComDataBits), "ComspecDataBits" + reader.Value, true); - else if (objectType == typeof(ComPort.eComHardwareHandshakeType)) - return Enum.Parse(typeof(ComPort.eComHardwareHandshakeType), "ComspecHardwareHandshake" + reader.Value, true); - else if (objectType == typeof(ComPort.eComParityType)) - return Enum.Parse(typeof(ComPort.eComParityType), "ComspecParity" + reader.Value, true); - else if (objectType == typeof(ComPort.eComProtocolType)) - return Enum.Parse(typeof(ComPort.eComProtocolType), "ComspecProtocol" + reader.Value, true); - else if (objectType == typeof(ComPort.eComSoftwareHandshakeType)) - return Enum.Parse(typeof(ComPort.eComSoftwareHandshakeType), "ComspecSoftwareHandshake" + reader.Value, true); - else if (objectType == typeof(ComPort.eComStopBits)) - return Enum.Parse(typeof(ComPort.eComStopBits), "ComspecStopBits" + reader.Value, true); - return null; - } - - /// - /// WriteJson method - /// - /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } + return objectType == typeof(ComPort.ComPortSpec?); } + public override bool CanRead { get { return true; } } + /// + /// This converter will not be used for writing + /// + public override bool CanWrite { get { return false; } } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} + +/// +/// The gist of this converter: The comspec JSON comes in with normal values that need to be converted +/// into enum names. This converter takes the value and applies the appropriate enum's name prefix to the value +/// and then returns the enum value using Enum.Parse. NOTE: Does not write +/// +public class ComSpecPropsJsonConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ComPort.eComBaudRates) + || objectType == typeof(ComPort.eComDataBits) + || objectType == typeof(ComPort.eComParityType) + || objectType == typeof(ComPort.eComHardwareHandshakeType) + || objectType == typeof(ComPort.eComSoftwareHandshakeType) + || objectType == typeof(ComPort.eComProtocolType) + || objectType == typeof(ComPort.eComStopBits); + } + + public override bool CanRead { get { return true; } } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + //Debug.LogMessage(LogEventLevel.Verbose, "ReadJson type: " + objectType.Name); + if (objectType == typeof(ComPort.eComBaudRates)) + return Enum.Parse(typeof(ComPort.eComBaudRates), "ComspecBaudRate" + reader.Value, false); + else if (objectType == typeof(ComPort.eComDataBits)) + return Enum.Parse(typeof(ComPort.eComDataBits), "ComspecDataBits" + reader.Value, true); + else if (objectType == typeof(ComPort.eComHardwareHandshakeType)) + return Enum.Parse(typeof(ComPort.eComHardwareHandshakeType), "ComspecHardwareHandshake" + reader.Value, true); + else if (objectType == typeof(ComPort.eComParityType)) + return Enum.Parse(typeof(ComPort.eComParityType), "ComspecParity" + reader.Value, true); + else if (objectType == typeof(ComPort.eComProtocolType)) + return Enum.Parse(typeof(ComPort.eComProtocolType), "ComspecProtocol" + reader.Value, true); + else if (objectType == typeof(ComPort.eComSoftwareHandshakeType)) + return Enum.Parse(typeof(ComPort.eComSoftwareHandshakeType), "ComspecSoftwareHandshake" + reader.Value, true); + else if (objectType == typeof(ComPort.eComStopBits)) + return Enum.Parse(typeof(ComPort.eComStopBits), "ComspecStopBits" + reader.Value, true); + return null; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs index 9318e04b..c6a10622 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs @@ -8,8 +8,8 @@ using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// /// @@ -59,43 +59,35 @@ namespace PepperDash.Essentials.Core case eControlMethod.Com: comm = new ComPortController(deviceConfig.Key + "-com", GetComPort, controlConfig.ComParams.Value, controlConfig); break; - case eControlMethod.ComBridge: - comm = new CommBridge(deviceConfig.Key + "-simpl", deviceConfig.Name + " Simpl"); - break; - case eControlMethod.Cec: - comm = new CecPortController(deviceConfig.Key + "-cec", GetCecPort, controlConfig); - break; + case eControlMethod.Cec: + comm = new CecPortController(deviceConfig.Key + "-cec", GetCecPort, controlConfig); + break; case eControlMethod.IR: break; - case eControlMethod.Ssh: - { - 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; - break; - } - case eControlMethod.Tcpip: - { - 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; - break; - } - case eControlMethod.Udp: - { - var udp = new GenericUdpServer(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize); - comm = udp; - break; - } + case eControlMethod.Ssh: + { + var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password); + ssh.AutoReconnect = c.AutoReconnect; + if(ssh.AutoReconnect) + ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; + comm = ssh; + break; + } + case eControlMethod.Tcpip: + { + var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize); + tcp.AutoReconnect = c.AutoReconnect; + if (tcp.AutoReconnect) + tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; + comm = tcp; + break; + } + case eControlMethod.Udp: + { + var udp = new GenericUdpServer(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize); + comm = udp; + break; + } case eControlMethod.Telnet: break; case eControlMethod.SecureTcpIp: @@ -138,19 +130,16 @@ namespace PepperDash.Essentials.Core return null; } - /// - /// Gets an ICec port from a RoutingInput or RoutingOutput on a device - /// - /// - /// - /// - /// GetCecPort method - /// - public static ICec GetCecPort(ControlPropertiesConfig config) - { - try - { - var dev = DeviceManager.GetDeviceForKey(config.ControlPortDevKey); + /// + /// Gets an ICec port from a RoutingInput or RoutingOutput on a device + /// + /// + /// + public static ICec GetCecPort(ControlPropertiesConfig config) + { + try + { + var dev = DeviceManager.GetDeviceForKey(config.ControlPortDevKey); Debug.LogMessage(LogEventLevel.Information, "GetCecPort: device '{0}' {1}", config.ControlPortDevKey, dev == null ? "is not valid, failed to get cec port" @@ -196,7 +185,7 @@ namespace PepperDash.Essentials.Core config.ControlPortDevKey, config.ControlPortName); return null; - } + } /// /// Helper to grab the IComPorts device for this PortDeviceKey. Key "controlSystem" will @@ -222,86 +211,65 @@ namespace PepperDash.Essentials.Core } } - /// - /// Represents a EssentialsControlPropertiesConfig - /// - public class EssentialsControlPropertiesConfig : - ControlPropertiesConfig - { - /// - /// Gets or sets the ComParams - /// - [JsonProperty("comParams", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(ComSpecJsonConverter))] - public ComPort.ComPortSpec? ComParams { get; set; } +/// +/// +/// +public class EssentialsControlPropertiesConfig : + ControlPropertiesConfig +{ - /// - /// Gets or sets the CresnetId - /// - [JsonProperty("cresnetId", NullValueHandling = NullValueHandling.Ignore)] - public string CresnetId { get; set; } + [JsonProperty("comParams", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(ComSpecJsonConverter))] + public ComPort.ComPortSpec? ComParams { get; set; } - /// - /// Attempts to provide uint conversion of string CresnetId - /// - [JsonIgnore] - public uint CresnetIdInt - { - get - { - try - { - return Convert.ToUInt32(CresnetId, 16); - } - catch (Exception) - { - throw new FormatException(string.Format("ERROR:Unable to convert Cresnet ID: {0} to hex. Error:\n{1}", CresnetId)); - } - } - } + [JsonProperty("cresnetId", NullValueHandling = NullValueHandling.Ignore)] + public string CresnetId { get; set; } - /// - /// Gets or sets the InfinetId - /// - [JsonProperty("infinetId", NullValueHandling = NullValueHandling.Ignore)] - public string InfinetId { get; set; } + /// + /// Attempts to provide uint conversion of string CresnetId + /// + [JsonIgnore] + public uint CresnetIdInt + { + get + { + try + { + return Convert.ToUInt32(CresnetId, 16); + } + catch (Exception) + { + throw new FormatException(string.Format("ERROR:Unable to convert Cresnet ID: {0} to hex. Error:\n{1}", CresnetId)); + } + } + } - /// - /// Attepmts to provide uiont conversion of string InifinetId - /// - [JsonIgnore] - public uint InfinetIdInt - { - get - { - try - { - return Convert.ToUInt32(InfinetId, 16); - } - catch (Exception) - { - throw new FormatException(string.Format("ERROR:Unable to conver Infinet ID: {0} to hex. Error:\n{1}", InfinetId)); - } - } - } - } + [JsonProperty("infinetId", NullValueHandling = NullValueHandling.Ignore)] + public string InfinetId { get; set; } - /// - /// Represents a IrControlSpec - /// - public class IrControlSpec - { - /// - /// Gets or sets the PortDeviceKey - /// - public string PortDeviceKey { get; set; } - /// - /// Gets or sets the PortNumber - /// - public uint PortNumber { get; set; } - /// - /// Gets or sets the File - /// - public string File { get; set; } - } + /// + /// Attepmts to provide uiont conversion of string InifinetId + /// + [JsonIgnore] + public uint InfinetIdInt + { + get + { + try + { + return Convert.ToUInt32(InfinetId, 16); + } + catch (Exception) + { + throw new FormatException(string.Format("ERROR:Unable to conver Infinet ID: {0} to hex. Error:\n{1}", InfinetId)); + } + } + } +} + +public class IrControlSpec +{ + public string PortDeviceKey { get; set; } + public uint PortNumber { get; set; } + public string File { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommunicationExtras.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommunicationExtras.cs index d9bd0974..cc188fa7 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CommunicationExtras.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommunicationExtras.cs @@ -13,8 +13,8 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// /// @@ -24,5 +24,4 @@ namespace PepperDash.Essentials.Core /// Gets the Device /// IComPorts Device { get; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ConsoleCommMockDevice.cs b/src/PepperDash.Essentials.Core/Comm and IR/ConsoleCommMockDevice.cs index 7c2af905..3f7f3097 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ConsoleCommMockDevice.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ConsoleCommMockDevice.cs @@ -9,11 +9,8 @@ using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a ConsoleCommMockDevice - /// +namespace PepperDash.Essentials.Core; + public class ConsoleCommMockDevice : EssentialsDevice, ICommunicationMonitor { /// @@ -103,31 +100,19 @@ namespace PepperDash.Essentials.Core } } - /// - /// Represents a ConsoleCommMockDeviceFactory - /// - public class ConsoleCommMockDeviceFactory : EssentialsDeviceFactory +public class ConsoleCommMockDeviceFactory : EssentialsDeviceFactory +{ + public ConsoleCommMockDeviceFactory() { - /// - /// Initializes a new instance of the ConsoleCommMockDeviceFactory class. - /// - public ConsoleCommMockDeviceFactory() - { - TypeNames = new List() { "commmock" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Comm Mock Device"); - var comm = CommFactory.CreateCommForDevice(dc); - var props = Newtonsoft.Json.JsonConvert.DeserializeObject( - dc.Properties.ToString()); - return new ConsoleCommMockDevice(dc.Key, dc.Name, props, comm); - } + TypeNames = new List() { "commmock" }; } + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Comm Mock Device"); + var comm = CommFactory.CreateCommForDevice(dc); + var props = Newtonsoft.Json.JsonConvert.DeserializeObject( + dc.Properties.ToString()); + return new ConsoleCommMockDevice(dc.Key, dc.Name, props, comm); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/GenericComm.cs b/src/PepperDash.Essentials.Core/Comm and IR/GenericComm.cs index 069244c3..6e913968 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/GenericComm.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/GenericComm.cs @@ -13,164 +13,132 @@ using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Serves as a generic wrapper class for all styles of IBasicCommuncation ports +/// +[Description("Generic communication wrapper class for any IBasicCommunication type")] +public class GenericComm : ReconfigurableBridgableDevice { - /// - /// Serves as a generic wrapper class for all styles of IBasicCommuncation ports - /// - [Description("Generic communication wrapper class for any IBasicCommunication type")] - public class GenericComm : ReconfigurableBridgableDevice + EssentialsControlPropertiesConfig PropertiesConfig; + + public IBasicCommunication CommPort { get; private set; } + + public GenericComm(DeviceConfig config) + : base(config) { - EssentialsControlPropertiesConfig PropertiesConfig; - /// - /// Gets the CommPort - /// - public IBasicCommunication CommPort { get; private set; } + PropertiesConfig = CommFactory.GetControlPropertiesConfig(config); - /// - /// Constructor - /// - /// the config of the device - public GenericComm(DeviceConfig config) - : base(config) + var commPort = CommFactory.CreateCommForDevice(config); + + //Fixing decision to require '-comPorts' in delcaration for DGE in order to get a device with comports included + if (commPort == null) { - - PropertiesConfig = CommFactory.GetControlPropertiesConfig(config); - - var commPort = CommFactory.CreateCommForDevice(config); - - //Fixing decision to require '-comPorts' in declaration for DGE in order to get a device with comports included - if (commPort == null) - { - config.Key = config.Key + "-comPorts"; - commPort = CommFactory.CreateCommForDevice(config); - } - - CommPort = commPort; - + config.Key = config.Key + "-comPorts"; + commPort = CommFactory.CreateCommForDevice(config); } - /// - /// BuildDevice method - /// - public static IKeyed BuildDevice(DeviceConfig dc) + CommPort = commPort; + + } + + public static IKeyed BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device"); + return new GenericComm(dc); + } + + public void SetPortConfig(string portConfig) + { + // TODO: Deserialize new EssentialsControlPropertiesConfig and handle as necessary + try { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device"); - return new GenericComm(dc); + PropertiesConfig = JsonConvert.DeserializeObject + (portConfig); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Error deserializing port config: {0}", e); + } + } + + protected override void CustomSetConfig(DeviceConfig config) + { + PropertiesConfig = CommFactory.GetControlPropertiesConfig(config); + + ConfigWriter.UpdateDeviceConfig(config); + } + + public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + var joinMap = new IBasicCommunicationJoinMap(joinStart); + + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + + if (bridge != null) + { + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); } - /// - /// SetPortConfig method - /// - public void SetPortConfig(string portConfig) + if (CommPort == null) { - // TODO: Deserialize new EssentialsControlPropertiesConfig and handle as necessary - try - { - PropertiesConfig = JsonConvert.DeserializeObject - (portConfig); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Error deserializing port config: {0}", e); - } + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to link device '{0}'. CommPort is null", Key); + return; } - /// - /// CustomSetConfig method - /// - /// the new device configuration - protected override void CustomSetConfig(DeviceConfig config) + Debug.LogMessage(LogEventLevel.Debug, this, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); + + // this is a permanent event handler. This cannot be -= from event + CommPort.TextReceived += (s, a) => { - PropertiesConfig = CommFactory.GetControlPropertiesConfig(config); + trilist.SetString(joinMap.TextReceived.JoinNumber, a.Text); + }; + trilist.SetStringSigAction(joinMap.SendText.JoinNumber, s => CommPort.SendText(s)); + trilist.SetStringSigAction(joinMap.SetPortConfig.JoinNumber, SetPortConfig); - ConfigWriter.UpdateDeviceConfig(config); - } - /// - /// LinkToApi method - /// - /// - public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + var sComm = CommPort as ISocketStatus; + if (sComm == null) return; + sComm.ConnectionChange += (s, a) => { - var joinMap = new IBasicCommunicationJoinMap(joinStart); + trilist.SetUshort(joinMap.Status.JoinNumber, (ushort)(a.Client.ClientStatus)); + trilist.SetBool(joinMap.Connected.JoinNumber, a.Client.ClientStatus == + SocketStatus.SOCKET_STATUS_CONNECTED); + }; - var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); - - if (!string.IsNullOrEmpty(joinMapSerialized)) - joinMap = JsonConvert.DeserializeObject(joinMapSerialized); - - if (bridge != null) + trilist.SetBoolSigAction(joinMap.Connect.JoinNumber, b => + { + if (b) { - bridge.AddJoinMap(Key, joinMap); + sComm.Connect(); } else { - Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); + sComm.Disconnect(); } + }); + } +} - if (CommPort == null) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to link device '{0}'. CommPort is null", Key); - return; - } - - Debug.LogMessage(LogEventLevel.Debug, this, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); - - // this is a permanent event handler. This cannot be -= from event - CommPort.TextReceived += (s, a) => - { - trilist.SetString(joinMap.TextReceived.JoinNumber, a.Text); - }; - trilist.SetStringSigAction(joinMap.SendText.JoinNumber, s => CommPort.SendText(s)); - trilist.SetStringSigAction(joinMap.SetPortConfig.JoinNumber, SetPortConfig); - - - var sComm = CommPort as ISocketStatus; - if (sComm == null) return; - sComm.ConnectionChange += (s, a) => - { - trilist.SetUshort(joinMap.Status.JoinNumber, (ushort)(a.Client.ClientStatus)); - trilist.SetBool(joinMap.Connected.JoinNumber, a.Client.ClientStatus == - SocketStatus.SOCKET_STATUS_CONNECTED); - }; - - trilist.SetBoolSigAction(joinMap.Connect.JoinNumber, b => - { - if (b) - { - sComm.Connect(); - } - else - { - sComm.Disconnect(); - } - }); - } +public class GenericCommFactory : EssentialsDeviceFactory +{ + public GenericCommFactory() + { + TypeNames = new List() { "genericComm" }; } - /// - /// Represents a GenericCommFactory - /// - public class GenericCommFactory : EssentialsDeviceFactory + public override EssentialsDevice BuildDevice(DeviceConfig dc) { - /// - /// Initializes a new instance of the GenericCommFactory class. - /// - public GenericCommFactory() - { - TypeNames = new List() { "genericComm" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device"); - return new GenericComm(dc); - } + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device"); + return new GenericComm(dc); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs b/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs index e02027b3..d1c3a7c8 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs @@ -2,13 +2,9 @@ using PepperDash.Core; using System; -namespace PepperDash.Essentials.Core -{ - - /// - /// Represents a GenericHttpClient - /// - [Obsolete("Please use the builtin HttpClient class instead: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines")] +namespace PepperDash.Essentials.Core; + +[Obsolete("Please use the builtin HttpClient class instead: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines")] public class GenericHttpClient : Device, IBasicCommunication { private readonly HttpClient Client; @@ -27,13 +23,13 @@ namespace PepperDash.Essentials.Core public GenericHttpClient(string key, string name, string hostname) : base(key, name) { - Client = new HttpClient - { - HostName = hostname - }; + Client = new HttpClient + { + HostName = hostname + }; - } + } /// /// SendText method @@ -87,8 +83,8 @@ namespace PepperDash.Essentials.Core if (responseReceived.ContentString.Length > 0) { - ResponseRecived?.Invoke(this, new GenericHttpClientEventArgs(responseReceived.ContentString, (request as HttpClientRequest).Url.ToString(), error)); - } + ResponseRecived?.Invoke(this, new GenericHttpClientEventArgs(responseReceived.ContentString, (request as HttpClientRequest).Url.ToString(), error)); + } } } @@ -180,5 +176,4 @@ namespace PepperDash.Essentials.Core RequestPath = request; Error = error; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/IRPortHelper.cs b/src/PepperDash.Essentials.Core/Comm and IR/IRPortHelper.cs index 6ca4b533..b4d4069d 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/IRPortHelper.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/IRPortHelper.cs @@ -12,8 +12,8 @@ using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Helper class for IR port operations /// @@ -145,7 +145,7 @@ namespace PepperDash.Essentials.Core var port = irDev.IROutputPorts[portNum]; - + return port; } @@ -157,82 +157,82 @@ namespace PepperDash.Essentials.Core /// IrOutputPortController object public static IrOutputPortController GetIrOutputPortController(DeviceConfig config) { - Debug.LogMessage(LogEventLevel.Debug, "Attempting to create new Ir Port Controller"); + Debug.LogMessage(LogEventLevel.Debug, "Attempting to create new Ir Port Controller"); if (config == null) { return null; } - var postActivationFunc = new Func (GetIrOutputPort); + var postActivationFunc = new Func (GetIrOutputPort); var irDevice = new IrOutputPortController(config.Key + "-ir", postActivationFunc, config); return irDevice; } /* - /// - /// GetIrOutputPortController method - /// - public static IrOutputPortController GetIrOutputPortController(DeviceConfig devConf) + /// + /// Returns a ready-to-go IrOutputPortController from a DeviceConfig object. + /// + public static IrOutputPortController GetIrOutputPortController(DeviceConfig devConf) + { + var irControllerKey = devConf.Key + "-ir"; + if (devConf.Properties == null) { - var irControllerKey = devConf.Key + "-ir"; - if (devConf.Properties == null) - { - Debug.LogMessage(LogEventLevel.Information, "[{0}] WARNING: Device config does not include properties. IR will not function.", devConf.Key); - return new IrOutputPortController(irControllerKey, null, ""); - } + Debug.LogMessage(LogEventLevel.Information, "[{0}] WARNING: Device config does not include properties. IR will not function.", devConf.Key); + return new IrOutputPortController(irControllerKey, null, ""); + } - var control = devConf.Properties["control"]; - if (control == null) - { - var c = new IrOutputPortController(irControllerKey, null, ""); - Debug.LogMessage(LogEventLevel.Information, c, "WARNING: Device config does not include control properties. IR will not function"); - return c; - } + var control = devConf.Properties["control"]; + if (control == null) + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.LogMessage(LogEventLevel.Information, c, "WARNING: Device config does not include control properties. IR will not function"); + return c; + } - var portDevKey = control.Value("controlPortDevKey"); - var portNum = control.Value("controlPortNumber"); - IIROutputPorts irDev = null; + var portDevKey = control.Value("controlPortDevKey"); + var portNum = control.Value("controlPortNumber"); + IIROutputPorts irDev = null; - if (portDevKey == null) - { - var c = new IrOutputPortController(irControllerKey, null, ""); - Debug.LogMessage(LogEventLevel.Information, c, "WARNING: control properties is missing ir device"); - return c; - } + if (portDevKey == null) + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.LogMessage(LogEventLevel.Information, c, "WARNING: control properties is missing ir device"); + return c; + } - if (portNum == 0) - { - var c = new IrOutputPortController(irControllerKey, null, ""); - Debug.LogMessage(LogEventLevel.Information, c, "WARNING: control properties is missing ir port number"); - return c; - } + if (portNum == 0) + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.LogMessage(LogEventLevel.Information, c, "WARNING: control properties is missing ir port number"); + return c; + } - if (portDevKey.Equals("controlSystem", StringComparison.OrdinalIgnoreCase) - || portDevKey.Equals("processor", StringComparison.OrdinalIgnoreCase)) - irDev = Global.ControlSystem; - else - irDev = DeviceManager.GetDeviceForKey(portDevKey) as IIROutputPorts; + if (portDevKey.Equals("controlSystem", StringComparison.OrdinalIgnoreCase) + || portDevKey.Equals("processor", StringComparison.OrdinalIgnoreCase)) + irDev = Global.ControlSystem; + else + irDev = DeviceManager.GetDeviceForKey(portDevKey) as IIROutputPorts; - if (irDev == null) - { - var c = new IrOutputPortController(irControllerKey, null, ""); - Debug.LogMessage(LogEventLevel.Information, c, "WARNING: device with IR ports '{0}' not found", portDevKey); - return c; - } + if (irDev == null) + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.LogMessage(LogEventLevel.Information, c, "WARNING: device with IR ports '{0}' not found", portDevKey); + return c; + } - if (portNum <= irDev.NumberOfIROutputPorts) // success! - return new IrOutputPortController(irControllerKey, irDev.IROutputPorts[portNum], - IrDriverPathPrefix + control["irFile"].Value()); - else - { - var c = new IrOutputPortController(irControllerKey, null, ""); - Debug.LogMessage(LogEventLevel.Information, c, "WARNING: device '{0}' IR port {1} out of range", - portDevKey, portNum); - return c; - } - }*/ + if (portNum <= irDev.NumberOfIROutputPorts) // success! + return new IrOutputPortController(irControllerKey, irDev.IROutputPorts[portNum], + IrDriverPathPrefix + control["irFile"].Value()); + else + { + var c = new IrOutputPortController(irControllerKey, null, ""); + Debug.LogMessage(LogEventLevel.Information, c, "WARNING: device '{0}' IR port {1} out of range", + portDevKey, portNum); + return c; + } + }*/ } /// @@ -265,5 +265,4 @@ namespace PepperDash.Essentials.Core { FileName = ""; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/AudioControlPointListItem.cs b/src/PepperDash.Essentials.Core/Config/AudioControlPointListItem.cs index 54cc3574..3a3e2fc8 100644 --- a/src/PepperDash.Essentials.Core/Config/AudioControlPointListItem.cs +++ b/src/PepperDash.Essentials.Core/Config/AudioControlPointListItem.cs @@ -6,24 +6,14 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core.Config +namespace PepperDash.Essentials.Core.Config; + +public class AudioControlPointListItem { - /// - /// Represents a AudioControlPointListItem - /// - public class AudioControlPointListItem - { - /// - /// Level controls for this audio control point - /// - [JsonProperty("levelControls")] - public Dictionary LevelControls { get; set; } = new Dictionary(); + [JsonProperty("levelControls")] + public Dictionary LevelControls { get; set; } = new Dictionary(); - /// - /// Presets for this audio control point - /// - [JsonProperty("presets")] - public Dictionary Presets { get; set; } = new Dictionary(); + [JsonProperty("presets")] + public Dictionary Presets { get; set; } = new Dictionary(); - } } diff --git a/src/PepperDash.Essentials.Core/Config/BasicConfig.cs b/src/PepperDash.Essentials.Core/Config/BasicConfig.cs index 435d448f..2e499ed4 100644 --- a/src/PepperDash.Essentials.Core/Config/BasicConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/BasicConfig.cs @@ -7,8 +7,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core.Devices; -namespace PepperDash.Essentials.Core.Config -{ +namespace PepperDash.Essentials.Core.Config; + /// /// Override this and splice on specific room type behavior, as well as other properties /// @@ -32,50 +32,32 @@ namespace PepperDash.Essentials.Core.Config [JsonProperty("sourceLists")] public Dictionary> SourceLists { get; set; } - /// - /// Gets or sets the DestinationLists - /// - [JsonProperty("destinationLists")] - public Dictionary> DestinationLists { get; set; } + [JsonProperty("destinationLists")] + public Dictionary> DestinationLists { get; set; } - /// - /// Gets or sets the AudioControlPointLists - /// - [JsonProperty("audioControlPointLists")] - public Dictionary AudioControlPointLists { get; set; } + [JsonProperty("audioControlPointLists")] + public Dictionary AudioControlPointLists { get; set; } - /// - /// Gets or sets the CameraLists - /// - [JsonProperty("cameraLists")] - public Dictionary> CameraLists { get; set; } + [JsonProperty("cameraLists")] + public Dictionary> CameraLists { get; set; } - /// - /// Gets or sets the TieLines - /// - [JsonProperty("tieLines")] - public List TieLines { get; set; } + [JsonProperty("tieLines")] + public List TieLines { get; set; } - /// - /// Gets or sets the JoinMaps - /// - [JsonProperty("joinMaps")] - public Dictionary JoinMaps { get; set; } + [JsonProperty("joinMaps")] + public Dictionary JoinMaps { get; set; } - /// - /// BasicConfig Constructor - /// - public BasicConfig() - { - Info = new InfoConfig(); - Devices = new List(); - SourceLists = new Dictionary>(); - DestinationLists = new Dictionary>(); - AudioControlPointLists = new Dictionary(); - CameraLists = new Dictionary>(); - TieLines = new List(); - JoinMaps = new Dictionary(); - } + public BasicConfig() + { + Info = new InfoConfig(); + Devices = new List(); + SourceLists = new Dictionary>(); + DestinationLists = new Dictionary>(); + AudioControlPointLists = new Dictionary(); + CameraLists = new Dictionary>(); + TieLines = new List(); + JoinMaps = new Dictionary(); + } /// /// Checks SourceLists for a given list and returns it if found. Otherwise, returns null @@ -88,11 +70,11 @@ namespace PepperDash.Essentials.Core.Config return SourceLists[key]; } - /// - /// Retrieves a DestinationListItem based on the key - /// - /// key of the list to retrieve - /// DestinationList if the key exists, null otherwise + /// + /// Retrieves a DestinationListItem based on the key + /// + /// key of the list to retrieve + /// DestinationList if the key exists, null otherwise public Dictionary GetDestinationListForKey(string key) { if (DestinationLists == null || string.IsNullOrEmpty(key) || !DestinationLists.ContainsKey(key)) @@ -103,51 +85,47 @@ namespace PepperDash.Essentials.Core.Config return DestinationLists[key]; } - /// - /// Retrieves a AudioControlPointList based on the key - /// - /// key of the list to retrieve - /// AudioControlPointList if the key exists, null otherwise - /// - /// GetAudioControlPointListForKey method - /// - public AudioControlPointListItem GetAudioControlPointListForKey(string key) + /// + /// Retrieves a AudioControlPointList based on the key + /// + /// key of the list to retrieve + /// AudioControlPointList if the key exists, null otherwise + public AudioControlPointListItem GetAudioControlPointListForKey(string key) + { + if (AudioControlPointLists == null || string.IsNullOrEmpty(key) || !AudioControlPointLists.ContainsKey(key)) + return null; + + return AudioControlPointLists[key]; + } + + /// + /// Checks CameraLists for a given list and returns it if found. Otherwise, returns null + /// + public Dictionary GetCameraListForKey(string key) + { + if (CameraLists == null || string.IsNullOrEmpty(key) || !CameraLists.ContainsKey(key)) + return null; + + return CameraLists[key]; + } + + /// + /// Checks Devices for an item with a Key that matches and returns it if found. Otherwise, retunes null + /// + /// Key of desired device + /// + public DeviceConfig GetDeviceForKey(string key) + { + if (string.IsNullOrEmpty(key)) + return null; + + var deviceConfig = Devices.FirstOrDefault(d => d.Key.Equals(key)); + + if (deviceConfig != null) + return deviceConfig; + else { - if (AudioControlPointLists == null || string.IsNullOrEmpty(key) || !AudioControlPointLists.ContainsKey(key)) - return null; - - return AudioControlPointLists[key]; - } - - /// - /// Checks CameraLists for a given list and returns it if found. Otherwise, returns null - /// - /// Key of desired camera list - public Dictionary GetCameraListForKey(string key) - { - if (CameraLists == null || string.IsNullOrEmpty(key) || !CameraLists.ContainsKey(key)) - return null; - - return CameraLists[key]; - } - - /// - /// Checks Devices for an item with a Key that matches and returns it if found. Otherwise, retunes null - /// - /// Key of desired device - public DeviceConfig GetDeviceForKey(string key) - { - if (string.IsNullOrEmpty(key)) - return null; - - var deviceConfig = Devices.FirstOrDefault(d => d.Key.Equals(key)); - - if (deviceConfig != null) - return deviceConfig; - else - { - return null; - } + return null; } + } } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/ConfigPropertiesHelpers.cs b/src/PepperDash.Essentials.Core/Config/ConfigPropertiesHelpers.cs index 99633266..9eb8e61d 100644 --- a/src/PepperDash.Essentials.Core/Config/ConfigPropertiesHelpers.cs +++ b/src/PepperDash.Essentials.Core/Config/ConfigPropertiesHelpers.cs @@ -9,27 +9,23 @@ using PepperDash.Essentials.Core; using PepperDash.Core; using Newtonsoft.Json.Linq; -namespace PepperDash.Essentials.Core.Config +namespace PepperDash.Essentials.Core.Config; + +public class ConfigPropertiesHelpers { /// - /// Represents a ConfigPropertiesHelpers + /// Returns the value of properties.hasAudio, or false if not defined /// - public class ConfigPropertiesHelpers + public static bool GetHasAudio(DeviceConfig deviceConfig) { - /// - /// GetHasAudio method - /// - public static bool GetHasAudio(DeviceConfig deviceConfig) - { - return deviceConfig.Properties.Value("hasAudio"); - } + return deviceConfig.Properties.Value("hasAudio"); + } - /// - /// Returns the value of properties.hasControls, or false if not defined - /// - public static bool GetHasControls(DeviceConfig deviceConfig) - { - return deviceConfig.Properties.Value("hasControls"); - } + /// + /// Returns the value of properties.hasControls, or false if not defined + /// + public static bool GetHasControls(DeviceConfig deviceConfig) + { + return deviceConfig.Properties.Value("hasControls"); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/DeviceConfig.cs b/src/PepperDash.Essentials.Core/Config/DeviceConfig.cs index b8620bb9..49994abc 100644 --- a/src/PepperDash.Essentials.Core/Config/DeviceConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/DeviceConfig.cs @@ -11,116 +11,71 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.Config +namespace PepperDash.Essentials.Core.Config; + +public class DeviceConfig { - /// - /// Represents a DeviceConfig - /// - public class DeviceConfig + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("uid")] + public int Uid { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("group")] + public string Group { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("properties")] + [JsonConverter(typeof(DevicePropertiesConverter))] + public JToken Properties { get; set; } + + public DeviceConfig(DeviceConfig dc) { - /// - /// Gets or sets the Key - /// - [JsonProperty("key")] - public string Key { get; set; } + Key = dc.Key; + Uid = dc.Uid; + Name = dc.Name; + Group = dc.Group; + Type = dc.Type; - /// - /// Gets or sets the Uid - /// - [JsonProperty("uid")] - public int Uid { get; set; } + Properties = JToken.Parse(dc.Properties.ToString()); - /// - /// Gets or sets the Name - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// Gets or sets the Group - /// - [JsonProperty("group")] - public string Group { get; set; } - - /// - /// Gets or sets the Type - /// - [JsonProperty("type")] - public string Type { get; set; } - - /// - /// Gets or sets the Properties - /// - [JsonProperty("properties")] - [JsonConverter(typeof(DevicePropertiesConverter))] - public JToken Properties { get; set; } - - /// - /// Constructor - /// - /// device config - public DeviceConfig(DeviceConfig dc) - { - Key = dc.Key; - Uid = dc.Uid; - Name = dc.Name; - Group = dc.Group; - Type = dc.Type; - - Properties = JToken.Parse(dc.Properties.ToString()); - - //Properties = JToken.FromObject(dc.Properties); - } - - /// - /// Default Constructor - /// - public DeviceConfig() { } + //Properties = JToken.FromObject(dc.Properties); } - /// - /// Represents a DevicePropertiesConverter - /// - public class DevicePropertiesConverter : JsonConverter + public DeviceConfig() {} +} + +/// +/// +/// +public class DevicePropertiesConverter : JsonConverter +{ + + public override bool CanConvert(Type objectType) { + return objectType == typeof(JToken); + } - /// - /// CanConvert method - /// - public override bool CanConvert(Type objectType) - { - return objectType == typeof(JToken); - } + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return JToken.ReadFrom(reader); + } - /// - /// ReadJson method - /// - /// reader to use - /// type of object being read - /// existing value for the object - /// serializer to use - /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override bool CanWrite + { + get { - return JToken.ReadFrom(reader); + return false; } + } - /// - public override bool CanWrite - { - get - { - return false; - } - } - - /// - /// WriteJson method - /// - /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException("SOD OFF HOSER"); - } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException("SOD OFF HOSER"); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index 40c6c0cf..071e5a20 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -11,8 +11,8 @@ using PepperDash.Core; using PepperDash.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core.Config -{ +namespace PepperDash.Essentials.Core.Config; + /// /// Loads the ConfigObject from the file /// @@ -22,7 +22,7 @@ namespace PepperDash.Essentials.Core.Config /// Local Config Present Message /// public const string LocalConfigPresent = - @" + @" *************************************************** ************* Using Local config file ************* ***************************************************"; @@ -40,88 +40,92 @@ namespace PepperDash.Essentials.Core.Config Debug.LogMessage(LogEventLevel.Information, "Loading unmerged system/template portal configuration file."); try { - // Check for local config file first - var filePath = Global.FilePathPrefix + ConfigWriter.LocalConfigFolder + Global.DirectorySeparator + Global.ConfigFileName; + // Check for local config file first + var filePath = Global.FilePathPrefix + ConfigWriter.LocalConfigFolder + Global.DirectorySeparator + Global.ConfigFileName; - bool localConfigFound = false; + bool localConfigFound = false; - Debug.LogMessage(LogEventLevel.Information, "Attempting to load Local config file: '{0}'", filePath); + Debug.LogMessage(LogEventLevel.Information, "Attempting to load Local config file: '{0}'", filePath); - // Check for local config directory first + // Check for local config directory first - var configFiles = GetConfigFiles(filePath); + var configFiles = GetConfigFiles(filePath); + + if (configFiles != null) + { + if (configFiles.Length > 1) + { + Debug.LogMessage(LogEventLevel.Information, + "****Error: Multiple Local Configuration files present. Please ensure only a single file exists and reset program.****"); + return false; + } + if(configFiles.Length == 1) + { + localConfigFound = true; + + } + } + else + { + Debug.LogMessage(LogEventLevel.Information, + "Local Configuration file not present.", filePath); + + } + + // Check for Portal Config + if(!localConfigFound) + { + filePath = Global.FilePathPrefix + Global.ConfigFileName; + + Debug.LogMessage(LogEventLevel.Information, "Attempting to load Portal config file: '{0}'", filePath); + + configFiles = GetConfigFiles(filePath); if (configFiles != null) { + Debug.LogMessage(LogEventLevel.Verbose, "{0} config files found matching pattern", configFiles.Length); + if (configFiles.Length > 1) { Debug.LogMessage(LogEventLevel.Information, - "****Error: Multiple Local Configuration files present. Please ensure only a single file exists and reset program.****"); + "****Error: Multiple Portal Configuration files present. Please ensure only a single file exists and reset program.****"); return false; } - if(configFiles.Length == 1) + else if (configFiles.Length == 1) { - localConfigFound = true; - + Debug.LogMessage(LogEventLevel.Information, "Found Portal config file: '{0}'", filePath); + } + else + { + Debug.LogMessage(LogEventLevel.Information, "No config file found."); + return false; } } else { Debug.LogMessage(LogEventLevel.Information, - "Local Configuration file not present.", filePath); - + "ERROR: Portal Configuration file not present. Please load file and reset program."); + return false; } + } - // Check for Portal Config - if(!localConfigFound) - { - filePath = Global.FilePathPrefix + Global.ConfigFileName; + // Get the actual file path + filePath = configFiles[0].FullName; - Debug.LogMessage(LogEventLevel.Information, "Attempting to load Portal config file: '{0}'", filePath); - - configFiles = GetConfigFiles(filePath); - - if (configFiles != null) - { - Debug.LogMessage(LogEventLevel.Verbose, "{0} config files found matching pattern", configFiles.Length); - - if (configFiles.Length > 1) - { - Debug.LogMessage(LogEventLevel.Information, - "****Error: Multiple Portal Configuration files present. Please ensure only a single file exists and reset program.****"); - return false; - } - else if (configFiles.Length == 1) - { - Debug.LogMessage(LogEventLevel.Information, "Found Portal config file: '{0}'", filePath); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "No config file found."); - return false; - } - } - else - { - Debug.LogMessage(LogEventLevel.Information, - "ERROR: Portal Configuration file not present. Please load file and reset program."); - return false; - } - } - - // Get the actual file path - filePath = configFiles[0].FullName; - - // Generate debug statement if using a local file. + // Generate debug statement if using a local file. if (localConfigFound) { - GetLocalFileMessage(filePath); + GetLocalFileMessage(filePath); } - // Read the file - using (StreamReader fs = new StreamReader(filePath)) + // Read the file + using (StreamReader fs = new StreamReader(filePath)) + { + Debug.LogMessage(LogEventLevel.Information, "Loading config file: '{0}'", filePath); + + if (localConfigFound) { - Debug.LogMessage(LogEventLevel.Information, "Loading config file: '{0}'", filePath); + ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject(); if (localConfigFound) { @@ -166,114 +170,135 @@ namespace PepperDash.Essentials.Core.Config return true; } + else + { + var doubleObj = JObject.Parse(fs.ReadToEnd()); + ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject(); + + // Extract SystemUrl and TemplateUrl into final config output + + if (doubleObj["system_url"] != null) + { + ConfigObject.SystemUrl = doubleObj["system_url"].Value(); + } + + if (doubleObj["template_url"] != null) + { + ConfigObject.TemplateUrl = doubleObj["template_url"].Value(); + } + } + + Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config"); + + return true; + } } catch (Exception e) { - Debug.LogMessage(LogEventLevel.Information, "ERROR: Config load failed: \r{0}", e); + Debug.LogMessage(LogEventLevel.Information, "ERROR: Config load failed: \r{0}", e); return false; } } - /// - /// Returns all the files from the directory specified. - /// - /// path to the directory - /// config files - public static FileInfo[] GetConfigFiles(string filePath) + /// + /// Returns all the files from the directory specified. + /// + /// + /// + public static FileInfo[] GetConfigFiles(string filePath) + { + // Get the directory + var dir = Path.GetDirectoryName(filePath); + + if (Directory.Exists(dir)) { - // Get the directory - var dir = Path.GetDirectoryName(filePath); + Debug.LogMessage(LogEventLevel.Debug, "Searching in Directory '{0}'", dir); + // Get the directory info + var dirInfo = new DirectoryInfo(dir); - if (Directory.Exists(dir)) - { - Debug.LogMessage(LogEventLevel.Debug, "Searching in Directory '{0}'", dir); - // Get the directory info - var dirInfo = new DirectoryInfo(dir); + // Get the file name + var fileName = Path.GetFileName(filePath); + Debug.LogMessage(LogEventLevel.Debug, "For Config Files matching: '{0}'", fileName); - // Get the file name - var fileName = Path.GetFileName(filePath); - Debug.LogMessage(LogEventLevel.Debug, "For Config Files matching: '{0}'", fileName); - - // Get the files that match from the directory - return dirInfo.GetFiles(fileName); - } - else - { - Debug.LogMessage(LogEventLevel.Information, - "Directory not found: ", dir); - - return null; - } + // Get the files that match from the directory + return dirInfo.GetFiles(fileName); } + else + { + Debug.LogMessage(LogEventLevel.Information, + "Directory not found: ", dir); + + return null; + } + } /// /// Returns the group for a given device key in config /// - /// Key of the device - /// Group name if the device is found, null otherwise - public static string GetGroupForDeviceKey(string key) - { - var dev = ConfigObject.Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - return dev == null ? null : dev.Group; - } + /// + /// + public static string GetGroupForDeviceKey(string key) + { + var dev = ConfigObject.Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + return dev == null ? null : dev.Group; + } private static void GetLocalFileMessage(string filePath) { - var filePathLength = filePath.Length + 2; - var debugStringWidth = filePathLength + 12; + var filePathLength = filePath.Length + 2; + var debugStringWidth = filePathLength + 12; - if (debugStringWidth < 51) - { - debugStringWidth = 51; - } - var qualifier = (filePathLength % 2 != 0) - ? " Using Local Config File " - : " Using Local Config File "; - var bookend1 = (debugStringWidth - qualifier.Length) / 2; - var bookend2 = (debugStringWidth - filePathLength) / 2; + if (debugStringWidth < 51) + { + debugStringWidth = 51; + } + var qualifier = (filePathLength % 2 != 0) + ? " Using Local Config File " + : " Using Local Config File "; + var bookend1 = (debugStringWidth - qualifier.Length) / 2; + var bookend2 = (debugStringWidth - filePathLength) / 2; var newDebugString = new StringBuilder() .Append(CrestronEnvironment.NewLine) - // Line 1 + // Line 1 .Append(new string('*', debugStringWidth)) .Append(CrestronEnvironment.NewLine) - // Line 2 + // Line 2 .Append(new string('*', debugStringWidth)) .Append(CrestronEnvironment.NewLine) - // Line 3 + // Line 3 .Append(new string('*', 2)) .Append(new string(' ', debugStringWidth - 4)) .Append(new string('*', 2)) .Append(CrestronEnvironment.NewLine) - // Line 4 + // Line 4 .Append(new string('*', 2)) .Append(new string(' ', bookend1 - 2)) .Append(qualifier) .Append(new string(' ', bookend1 - 2)) .Append(new string('*', 2)) .Append(CrestronEnvironment.NewLine) - // Line 5 + // Line 5 .Append(new string('*', 2)) .Append(new string(' ', bookend2 - 2)) .Append(" " + filePath + " ") .Append(new string(' ', bookend2 - 2)) .Append(new string('*', 2)) .Append(CrestronEnvironment.NewLine) - // Line 6 + // Line 6 .Append(new string('*', 2)) .Append(new string(' ', debugStringWidth - 4)) .Append(new string('*', 2)) .Append(CrestronEnvironment.NewLine) - // Line 7 + // Line 7 .Append(new string('*', debugStringWidth)) .Append(CrestronEnvironment.NewLine) - // Line 8 + // Line 8 .Append(new string('*', debugStringWidth)); - Debug.LogMessage(LogEventLevel.Verbose, "Found Local config file: '{0}'", filePath); - Debug.LogMessage(LogEventLevel.Information, newDebugString.ToString()); + Debug.LogMessage(LogEventLevel.Verbose, "Found Local config file: '{0}'", filePath); + Debug.LogMessage(LogEventLevel.Information, newDebugString.ToString()); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs index 9df673bf..a41c43e0 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs @@ -14,204 +14,194 @@ using Crestron.SimplSharpPro.Diagnostics; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.Config +namespace PepperDash.Essentials.Core.Config; + +public static class ConfigUpdater { - /// - /// ConfigUpdater class - /// - public static class ConfigUpdater + public static event EventHandler ConfigStatusChanged; + + public static void GetConfigFromServer(string url) { - /// - /// ConfigStatusChanged event - /// - public static event EventHandler ConfigStatusChanged; + Debug.LogMessage(LogEventLevel.Information, "Attempting to get new config from '{0}'", url); - /// - /// GetConfigFromServer method - /// - /// URL of the config server - public static void GetConfigFromServer(string url) + // HTTP GET + var req = new HttpClientRequest(); + + try { - Debug.LogMessage(LogEventLevel.Information, "Attempting to get new config from '{0}'", url); + req.RequestType = RequestType.Get; + req.Url.Parse(url); - // HTTP GET - var req = new HttpClientRequest(); - - try - { - req.RequestType = RequestType.Get; - req.Url.Parse(url); - - new HttpClient().DispatchAsync(req, (r, e) => + new HttpClient().DispatchAsync(req, (r, e) => + { + if (e == HTTP_CALLBACK_ERROR.COMPLETED) { - if (e == HTTP_CALLBACK_ERROR.COMPLETED) + if (r.Code == 200) { - if (r.Code == 200) - { - var newConfig = r.ContentString; + var newConfig = r.ContentString; - OnStatusUpdate(eUpdateStatus.ConfigFileReceived); + OnStatusUpdate(eUpdateStatus.ConfigFileReceived); - ArchiveExistingPortalConfigs(); + ArchiveExistingPortalConfigs(); - CheckForLocalConfigAndDelete(); + CheckForLocalConfigAndDelete(); - WriteConfigToFile(newConfig); + WriteConfigToFile(newConfig); - RestartProgram(); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Config Update Process Stopped. Failed to get config file from server: {0}", r.Code); - OnStatusUpdate(eUpdateStatus.UpdateFailed); - } + RestartProgram(); } else - Debug.LogMessage(LogEventLevel.Information, "Request for config from Server Failed: {0}", e); - }); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Debug, "Error Getting Config from Server: {0}", e); - } - - } - - static void OnStatusUpdate(eUpdateStatus status) - { - var handler = ConfigStatusChanged; - - if(handler != null) - { - handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status)); - } - } - - static void WriteConfigToFile(string configData) - { - var filePath = Global.FilePathPrefix+ "configurationFile-updated.json"; - - try - { - var config = JObject.Parse(configData).ToObject(); - - ConfigWriter.WriteFile(filePath, configData); - - OnStatusUpdate(eUpdateStatus.WritingConfigFile); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Debug, "Error parsing new config: {0}", e); - - OnStatusUpdate(eUpdateStatus.UpdateFailed); - } - } - - /// - /// Checks for any existing portal config files and archives them - /// - static void ArchiveExistingPortalConfigs() - { - var filePath = Global.FilePathPrefix + Global.ConfigFileName; - - var configFiles = ConfigReader.GetConfigFiles(filePath); - - if (configFiles != null) - { - Debug.LogMessage(LogEventLevel.Information, "Existing config files found. Moving to Archive folder."); - - OnStatusUpdate(eUpdateStatus.ArchivingConfigs); - - MoveFilesToArchiveFolder(configFiles); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "No Existing config files found in '{0}'. Nothing to archive", filePath); - } - } - - /// - /// Checks for presence of archive folder and if found deletes contents. - /// Moves any config files to the archive folder and adds a .bak suffix - /// - /// - static void MoveFilesToArchiveFolder(FileInfo[] files) - { - string archiveDirectoryPath = Global.FilePathPrefix + "archive"; - - if (!Directory.Exists(archiveDirectoryPath)) - { - // Directory does not exist, create it - Directory.Create(archiveDirectoryPath); - } - else - { - // Directory exists, first clear any contents - var archivedConfigFiles = ConfigReader.GetConfigFiles(archiveDirectoryPath + Global.DirectorySeparator + Global.ConfigFileName + ".bak"); - - if(archivedConfigFiles != null || archivedConfigFiles.Length > 0) - { - Debug.LogMessage(LogEventLevel.Information, "{0} Existing files found in archive folder. Deleting.", archivedConfigFiles.Length); - - for (int i = 0; i < archivedConfigFiles.Length; i++ ) - { - var file = archivedConfigFiles[i]; - Debug.LogMessage(LogEventLevel.Information, "Deleting archived file: '{0}'", file.FullName); - file.Delete(); + { + Debug.LogMessage(LogEventLevel.Information, "Config Update Process Stopped. Failed to get config file from server: {0}", r.Code); + OnStatusUpdate(eUpdateStatus.UpdateFailed); + } } - } - - } - - // Move any files from the program folder to the archive folder - foreach (var file in files) - { - Debug.LogMessage(LogEventLevel.Information, "Moving config file '{0}' to archive folder", file.FullName); - - // Moves the file and appends the .bak extension - var fileDest = archiveDirectoryPath + "/" + file.Name + ".bak"; - if(!File.Exists(fileDest)) - { - file.MoveTo(fileDest); - } - else - Debug.LogMessage(LogEventLevel.Information, "Cannot move file to archive folder. Existing file already exists with same name: '{0}'", fileDest); - } + else + Debug.LogMessage(LogEventLevel.Information, "Request for config from Server Failed: {0}", e); + }); } - - /// - /// Checks for LocalConfig folder in file system and deletes if found - /// - static void CheckForLocalConfigAndDelete() + catch (Exception e) { - var folderPath = Global.FilePathPrefix + ConfigWriter.LocalConfigFolder; - - if (Directory.Exists(folderPath)) - { - OnStatusUpdate(eUpdateStatus.DeletingLocalConfig); - Directory.Delete(folderPath); - Debug.LogMessage(LogEventLevel.Information, "Local Config Found in '{0}'. Deleting.", folderPath); - } - } - - /// - /// Connects to the processor via SSH and restarts the program - /// - static void RestartProgram() - { - Debug.LogMessage(LogEventLevel.Information, "Attempting to Reset Program"); - - OnStatusUpdate(eUpdateStatus.RestartingProgram); - - string response = string.Empty; - - CrestronConsole.SendControlSystemCommand(string.Format("progreset -p:{0}", InitialParametersClass.ApplicationNumber), ref response); - - Debug.LogMessage(LogEventLevel.Debug, "Console Response: {0}", response); + Debug.LogMessage(LogEventLevel.Debug, "Error Getting Config from Server: {0}", e); } } + static void OnStatusUpdate(eUpdateStatus status) + { + var handler = ConfigStatusChanged; + + if(handler != null) + { + handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status)); + } + } + + static void WriteConfigToFile(string configData) + { + var filePath = Global.FilePathPrefix+ "configurationFile-updated.json"; + + try + { + var config = JObject.Parse(configData).ToObject(); + + ConfigWriter.WriteFile(filePath, configData); + + OnStatusUpdate(eUpdateStatus.WritingConfigFile); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Debug, "Error parsing new config: {0}", e); + + OnStatusUpdate(eUpdateStatus.UpdateFailed); + } + } + + /// + /// Checks for any existing portal config files and archives them + /// + static void ArchiveExistingPortalConfigs() + { + var filePath = Global.FilePathPrefix + Global.ConfigFileName; + + var configFiles = ConfigReader.GetConfigFiles(filePath); + + if (configFiles != null) + { + Debug.LogMessage(LogEventLevel.Information, "Existing config files found. Moving to Archive folder."); + + OnStatusUpdate(eUpdateStatus.ArchivingConfigs); + + MoveFilesToArchiveFolder(configFiles); + } + else + { + Debug.LogMessage(LogEventLevel.Information, "No Existing config files found in '{0}'. Nothing to archive", filePath); + } + } + + /// + /// Checks for presence of archive folder and if found deletes contents. + /// Moves any config files to the archive folder and adds a .bak suffix + /// + /// + static void MoveFilesToArchiveFolder(FileInfo[] files) + { + string archiveDirectoryPath = Global.FilePathPrefix + "archive"; + + if (!Directory.Exists(archiveDirectoryPath)) + { + // Directory does not exist, create it + Directory.Create(archiveDirectoryPath); + } + else + { + // Directory exists, first clear any contents + var archivedConfigFiles = ConfigReader.GetConfigFiles(archiveDirectoryPath + Global.DirectorySeparator + Global.ConfigFileName + ".bak"); + + if(archivedConfigFiles != null || archivedConfigFiles.Length > 0) + { + Debug.LogMessage(LogEventLevel.Information, "{0} Existing files found in archive folder. Deleting.", archivedConfigFiles.Length); + + for (int i = 0; i < archivedConfigFiles.Length; i++ ) + { + var file = archivedConfigFiles[i]; + Debug.LogMessage(LogEventLevel.Information, "Deleting archived file: '{0}'", file.FullName); + file.Delete(); + } + } + + } + + // Move any files from the program folder to the archive folder + foreach (var file in files) + { + Debug.LogMessage(LogEventLevel.Information, "Moving config file '{0}' to archive folder", file.FullName); + + // Moves the file and appends the .bak extension + var fileDest = archiveDirectoryPath + "/" + file.Name + ".bak"; + if(!File.Exists(fileDest)) + { + file.MoveTo(fileDest); + } + else + Debug.LogMessage(LogEventLevel.Information, "Cannot move file to archive folder. Existing file already exists with same name: '{0}'", fileDest); + } + } + + /// + /// Checks for LocalConfig folder in file system and deletes if found + /// + static void CheckForLocalConfigAndDelete() + { + var folderPath = Global.FilePathPrefix + ConfigWriter.LocalConfigFolder; + + if (Directory.Exists(folderPath)) + { + OnStatusUpdate(eUpdateStatus.DeletingLocalConfig); + Directory.Delete(folderPath); + Debug.LogMessage(LogEventLevel.Information, "Local Config Found in '{0}'. Deleting.", folderPath); + } + } + + /// + /// Connects to the processor via SSH and restarts the program + /// + static void RestartProgram() + { + Debug.LogMessage(LogEventLevel.Information, "Attempting to Reset Program"); + + OnStatusUpdate(eUpdateStatus.RestartingProgram); + + string response = string.Empty; + + CrestronConsole.SendControlSystemCommand(string.Format("progreset -p:{0}", InitialParametersClass.ApplicationNumber), ref response); + + Debug.LogMessage(LogEventLevel.Debug, "Console Response: {0}", response); + } + +} + /// /// Enumeration of eUpdateStatus values /// @@ -258,23 +248,12 @@ namespace PepperDash.Essentials.Core.Config UpdateFailed } - /// - /// Represents a ConfigStatusEventArgs - /// - public class ConfigStatusEventArgs : EventArgs - { - /// - /// Gets or sets the UpdateStatus - /// - public eUpdateStatus UpdateStatus { get; private set; } +public class ConfigStatusEventArgs : EventArgs +{ + public eUpdateStatus UpdateStatus { get; private set; } - /// - /// ConfigStatusEventArgs Constructor - /// - /// - public ConfigStatusEventArgs(eUpdateStatus status) - { - UpdateStatus = status; - } + public ConfigStatusEventArgs(eUpdateStatus status) + { + UpdateStatus = status; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigWriter.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigWriter.cs index 93517a3c..a688b378 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigWriter.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigWriter.cs @@ -11,174 +11,154 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.Config +namespace PepperDash.Essentials.Core.Config; + +/// +/// Responsible for updating config at runtime, and writing the updates out to a local file +/// +public class ConfigWriter { - /// - /// Responsible for updating config at runtime, and writing the updates out to a local file - /// - public class ConfigWriter - { - /// - /// LocalConfigFolder constant - /// - public const string LocalConfigFolder = "LocalConfig"; + public const string LocalConfigFolder = "LocalConfig"; - /// - /// WriteTimeout constant - /// - public const long WriteTimeout = 30000; - - /// - /// WriteTimer variable - /// - public static CTimer WriteTimer; + public const long WriteTimeout = 30000; + public static CTimer WriteTimer; static CCriticalSection fileLock = new CCriticalSection(); - /// - /// Updates the config properties of a device - /// - /// The key of the device to update - /// The new properties for the device - /// True if the update was successful, otherwise false - public static bool UpdateDeviceProperties(string deviceKey, JToken properties) + /// + /// Updates the config properties of a device + /// + /// + /// + /// + public static bool UpdateDeviceProperties(string deviceKey, JToken properties) + { + bool success = false; + + // Get the current device config + var deviceConfig = ConfigReader.ConfigObject.Devices.FirstOrDefault(d => d.Key.Equals(deviceKey)); + + if (deviceConfig != null) { - bool success = false; + // Replace the current properties JToken with the new one passed into this method + deviceConfig.Properties = properties; - // Get the current device config - var deviceConfig = ConfigReader.ConfigObject.Devices.FirstOrDefault(d => d.Key.Equals(deviceKey)); + Debug.LogMessage(LogEventLevel.Debug, "Updated properties of device: '{0}'", deviceKey); - if (deviceConfig != null) - { - // Replace the current properties JToken with the new one passed into this method - deviceConfig.Properties = properties; - - Debug.LogMessage(LogEventLevel.Debug, "Updated properties of device: '{0}'", deviceKey); - - success = true; - } - - ResetTimer(); - - return success; + success = true; } - /// - /// UpdateDeviceConfig method - /// - /// The new device config - /// True if the update was successful, otherwise false - public static bool UpdateDeviceConfig(DeviceConfig config) + ResetTimer(); + + return success; + } + + public static bool UpdateDeviceConfig(DeviceConfig config) + { + bool success = false; + + var deviceConfigIndex = ConfigReader.ConfigObject.Devices.FindIndex(d => d.Key.Equals(config.Key)); + + if (deviceConfigIndex >= 0) { - bool success = false; + ConfigReader.ConfigObject.Devices[deviceConfigIndex] = config; - var deviceConfigIndex = ConfigReader.ConfigObject.Devices.FindIndex(d => d.Key.Equals(config.Key)); + Debug.LogMessage(LogEventLevel.Debug, "Updated config of device: '{0}'", config.Key); - if (deviceConfigIndex >= 0) - { - ConfigReader.ConfigObject.Devices[deviceConfigIndex] = config; - - Debug.LogMessage(LogEventLevel.Debug, "Updated config of device: '{0}'", config.Key); - - success = true; - } - - ResetTimer(); - - return success; + success = true; } - /// - /// UpdateRoomConfig method - /// - /// The new room config - /// True if the update was successful, otherwise false - public static bool UpdateRoomConfig(DeviceConfig config) - { - bool success = false; + ResetTimer(); + + return success; + } + + public static bool UpdateRoomConfig(DeviceConfig config) + { + bool success = false; var roomConfigIndex = ConfigReader.ConfigObject.Rooms.FindIndex(d => d.Key.Equals(config.Key)); if (roomConfigIndex >= 0) - { - ConfigReader.ConfigObject.Rooms[roomConfigIndex] = config; - - Debug.LogMessage(LogEventLevel.Debug, "Updated room of device: '{0}'", config.Key); - - success = true; - } - - ResetTimer(); - - return success; - } - - /// - /// Resets (or starts) the write timer - /// - static void ResetTimer() { - if (WriteTimer == null) - WriteTimer = new CTimer(WriteConfigFile, WriteTimeout); + ConfigReader.ConfigObject.Rooms[roomConfigIndex] = config; - WriteTimer.Reset(WriteTimeout); + Debug.LogMessage(LogEventLevel.Debug, "Updated room of device: '{0}'", config.Key); - Debug.LogMessage(LogEventLevel.Debug, "Config File write timer has been reset."); - } - - /// - /// Writes the current config to a file in the LocalConfig subfolder - /// - private static void WriteConfigFile(object o) - { - var filePath = Global.FilePathPrefix + LocalConfigFolder + Global.DirectorySeparator + "configurationFile.json"; - - var configData = JsonConvert.SerializeObject(ConfigReader.ConfigObject); - - WriteFile(filePath, configData); - } - - /// - /// Writes the current config data to a file - /// - /// The file path to write to - /// The config data to write - public static void WriteFile(string filePath, string configData) - { - if (WriteTimer != null) - WriteTimer.Stop(); - - Debug.LogMessage(LogEventLevel.Information, "Writing Configuration to file"); - - Debug.LogMessage(LogEventLevel.Information, "Attempting to write config file: '{0}'", filePath); - - try - { - if (fileLock.TryEnter()) - { - using (StreamWriter sw = new StreamWriter(filePath)) - { - sw.Write(configData); - sw.Flush(); - } - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Unable to enter FileLock"); - } - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, "Error: Config write failed: \r{0}", e); - } - finally - { - if (fileLock != null && !fileLock.Disposed) - fileLock.Leave(); - - } + success = true; } + ResetTimer(); + return success; } + + /// + /// Resets (or starts) the write timer + /// + static void ResetTimer() + { + if (WriteTimer == null) + WriteTimer = new CTimer(WriteConfigFile, WriteTimeout); + + WriteTimer.Reset(WriteTimeout); + + Debug.LogMessage(LogEventLevel.Debug, "Config File write timer has been reset."); + } + + /// + /// Writes the current config to a file in the LocalConfig subfolder + /// + /// + private static void WriteConfigFile(object o) + { + var filePath = Global.FilePathPrefix + LocalConfigFolder + Global.DirectorySeparator + "configurationFile.json"; + + var configData = JsonConvert.SerializeObject(ConfigReader.ConfigObject); + + WriteFile(filePath, configData); + } + + /// + /// Writes + /// + /// + /// + public static void WriteFile(string filePath, string configData) + { + if (WriteTimer != null) + WriteTimer.Stop(); + + Debug.LogMessage(LogEventLevel.Information, "Writing Configuration to file"); + + Debug.LogMessage(LogEventLevel.Information, "Attempting to write config file: '{0}'", filePath); + + try + { + if (fileLock.TryEnter()) + { + using (StreamWriter sw = new StreamWriter(filePath)) + { + sw.Write(configData); + sw.Flush(); + } + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Unable to enter FileLock"); + } + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, "Error: Config write failed: \r{0}", e); + } + finally + { + if (fileLock != null && !fileLock.Disposed) + fileLock.Leave(); + + } + } + + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index 6ffe07d2..d9a5918f 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -9,171 +9,139 @@ using Newtonsoft.Json; using PepperDash.Core; -namespace PepperDash.Essentials.Core.Config +namespace PepperDash.Essentials.Core.Config; + +/// +/// Loads the ConfigObject from the file +/// +public class EssentialsConfig : BasicConfig +{ + [JsonProperty("system_url")] + public string SystemUrl { get; set; } + + [JsonProperty("template_url")] + public string TemplateUrl { get; set; } + + /// + /// Gets the SystemUuid extracted from the SystemUrl + /// + [JsonProperty("systemUuid")] + public string SystemUuid + { + get + { + 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\/(.*)\/.*"); + string uuid = result.Groups[1].Value; + return uuid; + } + } + } + + /// + /// Gets the TemplateUuid extracted from the TemplateUrl + /// + [JsonProperty("templateUuid")] + public string TemplateUuid + { + get + { + 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\/(.*)\/.*"); + string uuid = result.Groups[2].Value; + return uuid; + } + } + } + + [JsonProperty("rooms")] + public List Rooms { get; set; } + + /// + /// Gets or sets the Versions + /// + public VersionData Versions { get; set; } + + public EssentialsConfig() + : base() + { + Rooms = new List(); + } +} + +/// +/// Represents version data for Essentials and its packages +/// +public class VersionData { /// - /// Loads the ConfigObject from the file + /// Gets or sets the Essentials version /// - public class EssentialsConfig : BasicConfig - { - /// - /// Gets or sets the SystemUrl - /// - [JsonProperty("system_url")] - public string SystemUrl { get; set; } - - /// - /// 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 - { - get - { - string uuid; - - if (string.IsNullOrEmpty(SystemUrl)) - { - uuid = "missing url"; - } - else if (SystemUrl.Contains("#")) - { - var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); - 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\/(.*)\/.*"); - uuid = result.Groups[1].Value; - } - - return uuid; - } - } - - /// - /// Gets the TemplateUuid extracted from the TemplateUrl - /// - [JsonProperty("templateUuid")] - public string TemplateUuid - { - get - { - string uuid; - - if (string.IsNullOrEmpty(TemplateUrl)) - { - uuid = "missing template url"; - } - else if (TemplateUrl.Contains("#")) - { - var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*"); - 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\/(.*)\/system-template-versions\/(.*)\/.*"); - uuid = result.Groups[2].Value; - } - - return uuid; - } - } - - /// - /// Gets or sets the Rooms - /// - [JsonProperty("rooms")] - public List Rooms { get; set; } - - /// - /// Gets or sets the Versions - /// - public VersionData Versions { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public EssentialsConfig() - : base() - { - Rooms = new List(); - } - } + [JsonProperty("essentials")] + public NugetVersion Essentials { get; set; } /// - /// Represents version data for Essentials and its packages + /// Gets or sets the list of 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(); - } - } + [JsonProperty("packages")] + public List Packages { get; set; } /// - /// Represents a NugetVersion + /// Initializes a new instance of the class. /// - public class NugetVersion + public VersionData() { - /// - /// Gets or sets the Version - /// - [JsonProperty("version")] - public string Version { get; set; } - - /// - /// Gets or sets the PackageId - /// - [JsonProperty("packageId")] - public string PackageId { get; set; } + Packages = new List(); } +} + +/// +/// Represents a NugetVersion +/// +public class NugetVersion +{ + /// + /// Gets or sets the Version + /// + [JsonProperty("version")] + public string Version { get; set; } /// - /// Represents a SystemTemplateConfigs + /// Gets or sets the PackageId /// - public class SystemTemplateConfigs - { - /// - /// Gets or sets the System - /// - public EssentialsConfig System { get; set; } + [JsonProperty("packageId")] + public string PackageId { get; set; } +} - /// - /// Gets or sets the Template - /// - public EssentialsConfig Template { get; set; } - } -} \ No newline at end of file +/// +/// +/// +public class SystemTemplateConfigs +{ + public EssentialsConfig System { get; set; } + + /// + /// Gets or sets the Template + /// + public EssentialsConfig Template { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Config/ILoadConfig.cs b/src/PepperDash.Essentials.Core/Config/ILoadConfig.cs index ed6ff61f..a02baa8c 100644 --- a/src/PepperDash.Essentials.Core/Config/ILoadConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/ILoadConfig.cs @@ -4,16 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public interface ILoadConfig { - /// - /// Defines the contract for ILoadConfig - /// - public interface ILoadConfig - { - /// - /// GoWithLoad method - /// - void GoWithLoad(); - } + void GoWithLoad(); } diff --git a/src/PepperDash.Essentials.Core/Config/InfoConfig.cs b/src/PepperDash.Essentials.Core/Config/InfoConfig.cs index 1ce2c60b..7c2f81e4 100644 --- a/src/PepperDash.Essentials.Core/Config/InfoConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/InfoConfig.cs @@ -5,12 +5,12 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core.Config -{ - /// - /// Represents the info section of a Config file - /// - public class InfoConfig +namespace PepperDash.Essentials.Core.Config; + +/// +/// Represents the info section of a Config file +/// +public class InfoConfig { /// /// Gets or sets the Name @@ -70,49 +70,49 @@ namespace PepperDash.Essentials.Core.Config Type = ""; Version = ""; Comment = ""; - HostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); - AppNumber = InitialParametersClass.ApplicationNumber; + HostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); + AppNumber = InitialParametersClass.ApplicationNumber; - RuntimeInfo = new RuntimeInfo(); + RuntimeInfo = new RuntimeInfo(); } } - + +/// +/// Represents runtime information about the program/processor +/// +public class RuntimeInfo +{ /// - /// Represents a RuntimeInfo + /// The name of the running application /// - public class RuntimeInfo - { - /// - /// The name of the running application - /// - [JsonProperty("appName")] - public string AppName {get; set;} - //{ - // get - // { - // return Assembly.GetExecutingAssembly().GetName().Name; - // } - //} + [JsonProperty("appName")] + public string AppName {get; set;} + //{ + // get + // { + // return Assembly.GetExecutingAssembly().GetName().Name; + // } + //} - /// - /// The Assembly version of the running application - /// - [JsonProperty("assemblyVersion")] - public string AssemblyVersion {get; set;} - //{ - // get - // { - // var version = Assembly.GetExecutingAssembly().GetName().Version; - // return string.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Build); - // } - //} + /// + /// The Assembly version of the running application + /// + [JsonProperty("assemblyVersion")] + public string AssemblyVersion {get; set;} + //{ + // get + // { + // var version = Assembly.GetExecutingAssembly().GetName().Version; + // return string.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Build); + // } + //} - /// , - /// The OS Version of the processor (Firmware Version) - /// - [JsonProperty("osVersion")] - public string OsVersion {get; set;} + /// , + /// The OS Version of the processor (Firmware Version) + /// + [JsonProperty("osVersion")] + public string OsVersion {get; set;} //{ // get // { @@ -120,17 +120,15 @@ namespace PepperDash.Essentials.Core.Config // } //} - /// - /// The information gathered by the processor at runtime about it's NICs and their IP addresses. - /// - [JsonProperty("ipInfo")] - public Dictionary IpInfo + /// + /// The information gathered by the processor at runtime about it's NICs and their IP addresses. + /// + [JsonProperty("ipInfo")] + public Dictionary IpInfo + { + get { - get - { - return Global.EthernetAdapterInfoCollection; - } + return Global.EthernetAdapterInfoCollection; } } - } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/SourceDevicePropertiesConfigBase.cs b/src/PepperDash.Essentials.Core/Config/SourceDevicePropertiesConfigBase.cs index 42a576a2..ae30de0a 100644 --- a/src/PepperDash.Essentials.Core/Config/SourceDevicePropertiesConfigBase.cs +++ b/src/PepperDash.Essentials.Core/Config/SourceDevicePropertiesConfigBase.cs @@ -4,16 +4,12 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core.Config +namespace PepperDash.Essentials.Core.Config; + +public class SourceDevicePropertiesConfigBase { - /// - /// Represents a SourceDevicePropertiesConfigBase - /// - public class SourceDevicePropertiesConfigBase - { - /// - /// Gets or sets the DisableSharing - /// - public bool DisableSharing { get; set; } - } + /// + /// Gets or sets the DisableSharing + /// + public bool DisableSharing { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs b/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs index bdb3e461..afb15015 100644 --- a/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs +++ b/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs @@ -7,11 +7,8 @@ using PepperDash.Core.JsonStandardObjects; using PepperDash.Essentials.Core.Bridges; using Serilog.Events; -namespace PepperDash.Essentials.Core -{ - /// - /// Abstract base class for Crestron GenericBase devices - /// +namespace PepperDash.Essentials.Core; + public abstract class CrestronGenericBaseDevice : EssentialsDevice, IOnline, IHasFeedback, ICommunicationMonitor, IUsageTracking { /// @@ -19,10 +16,10 @@ namespace PepperDash.Essentials.Core /// protected GenericBase Hardware; - /// - /// Gets or sets the Feedbacks - /// - public FeedbackCollection Feedbacks { get; private set; } + /// + /// Returns a list containing the Outputs that we want to expose. + /// + public FeedbackCollection Feedbacks { get; private set; } /// /// Gets or sets the IsOnline @@ -51,30 +48,25 @@ namespace PepperDash.Essentials.Core /// name of the device /// hardware instance protected CrestronGenericBaseDevice(string key, string name, GenericBase hardware) - : base(key, name) - { - Feedbacks = new FeedbackCollection(); + : base(key, name) + { + Feedbacks = new FeedbackCollection(); - Hardware = hardware; - IsOnline = new BoolFeedback("IsOnlineFeedback", () => Hardware.IsOnline); - IsRegistered = new BoolFeedback("IsRegistered", () => Hardware.Registered); - IpConnectionsText = new StringFeedback("IpConnectionsText", () => Hardware.ConnectedIpList != null ? string.Join(",", Hardware.ConnectedIpList.Select(cip => cip.DeviceIpAddress).ToArray()) : string.Empty); - AddToFeedbackList(IsOnline, IpConnectionsText); + Hardware = hardware; + IsOnline = new BoolFeedback("IsOnlineFeedback", () => Hardware.IsOnline); + IsRegistered = new BoolFeedback("IsRegistered", () => Hardware.Registered); + IpConnectionsText = new StringFeedback("IpConnectionsText", () => Hardware.ConnectedIpList != null ? string.Join(",", Hardware.ConnectedIpList.Select(cip => cip.DeviceIpAddress).ToArray()) : string.Empty); + AddToFeedbackList(IsOnline, IpConnectionsText); - CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, hardware, 120000, 300000); - } + CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, hardware, 120000, 300000); + } - /// - /// Constructor without hardware instance - /// - /// key of the device - /// name of the device - protected CrestronGenericBaseDevice(string key, string name) - : base(key, name) - { - Feedbacks = new FeedbackCollection(); + protected CrestronGenericBaseDevice(string key, string name) + : base(key, name) + { + Feedbacks = new FeedbackCollection(); - } + } /// /// Registers the Crestron GenericBase hardware instance @@ -82,13 +74,13 @@ namespace PepperDash.Essentials.Core /// hardware instance protected void RegisterCrestronGenericBase(GenericBase hardware) { - Hardware = hardware; - IsOnline = new BoolFeedback("IsOnlineFeedback", () => Hardware.IsOnline); - IsRegistered = new BoolFeedback("IsRegistered", () => Hardware.Registered); - IpConnectionsText = new StringFeedback("IpConnectionsText", () => Hardware.ConnectedIpList != null ? string.Join(",", Hardware.ConnectedIpList.Select(cip => cip.DeviceIpAddress).ToArray()) : string.Empty); - AddToFeedbackList(IsOnline, IpConnectionsText); + Hardware = hardware; + IsOnline = new BoolFeedback("IsOnlineFeedback", () => Hardware.IsOnline); + IsRegistered = new BoolFeedback("IsRegistered", () => Hardware.Registered); + IpConnectionsText = new StringFeedback("IpConnectionsText", () => Hardware.ConnectedIpList != null ? string.Join(",", Hardware.ConnectedIpList.Select(cip => cip.DeviceIpAddress).ToArray()) : string.Empty); + AddToFeedbackList(IsOnline, IpConnectionsText); - CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, hardware, 120000, 300000); + CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, hardware, 120000, 300000); } /// @@ -97,43 +89,43 @@ namespace PepperDash.Essentials.Core /// public override bool CustomActivate() { - Debug.LogMessage(LogEventLevel.Information, this, "Activating"); - if (!PreventRegistration) - { - //Debug.LogMessage(LogEventLevel.Debug, this, " Does not require registration. Skipping"); + Debug.LogMessage(LogEventLevel.Information, this, "Activating"); + if (!PreventRegistration) + { + //Debug.LogMessage(LogEventLevel.Debug, this, " Does not require registration. Skipping"); - if (Hardware.Registerable && !Hardware.Registered) + if (Hardware.Registerable && !Hardware.Registered) + { + var response = Hardware.RegisterWithLogging(Key); + if (response != eDeviceRegistrationUnRegistrationResponse.Success) { - var response = Hardware.RegisterWithLogging(Key); - if (response != eDeviceRegistrationUnRegistrationResponse.Success) - { - //Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Crestron device: {0}", response); - return false; - } + //Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Crestron device: {0}", response); + return false; } - - IsRegistered.FireUpdate(); } - else - { - AddPostActivationAction(() => + + IsRegistered.FireUpdate(); + } + else + { + AddPostActivationAction(() => + { + if (Hardware.Registerable && !Hardware.Registered) { - if (Hardware.Registerable && !Hardware.Registered) - { - var response = Hardware.RegisterWithLogging(Key); - } + var response = Hardware.RegisterWithLogging(Key); + } - IsRegistered.FireUpdate(); - }); - } + IsRegistered.FireUpdate(); + }); + } - foreach (var f in Feedbacks) - { - f.FireUpdate(); - } + foreach (var f in Feedbacks) + { + f.FireUpdate(); + } - Hardware.OnlineStatusChange += Hardware_OnlineStatusChange; - CommunicationMonitor.Start(); + Hardware.OnlineStatusChange += Hardware_OnlineStatusChange; + CommunicationMonitor.Start(); return base.CustomActivate(); } @@ -149,42 +141,42 @@ namespace PepperDash.Essentials.Core var success = Hardware.UnRegister() == eDeviceRegistrationUnRegistrationResponse.Success; - IsRegistered.FireUpdate(); + IsRegistered.FireUpdate(); - return success; + return success; } /// - /// Adds feedback(s) to the list - /// - /// feedback(s) to be added to the list - public void AddToFeedbackList(params Feedback[] newFbs) + /// Adds feedback(s) to the list + /// + /// + public void AddToFeedbackList(params Feedback[] newFbs) + { + foreach (var f in newFbs) { - foreach (var f in newFbs) - { - if (f == null) continue; + if (f == null) continue; - if (!Feedbacks.Contains(f)) - { - Feedbacks.Add(f); - } + if (!Feedbacks.Contains(f)) + { + Feedbacks.Add(f); } } + } void Hardware_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) { - Debug.LogMessage(LogEventLevel.Verbose, this, "OnlineStatusChange Event. Online = {0}", args.DeviceOnLine); + Debug.LogMessage(LogEventLevel.Verbose, this, "OnlineStatusChange Event. Online = {0}", args.DeviceOnLine); - if (!Hardware.Registered) - { - return; // protects in cases where device has been unregistered and feedbacks would attempt to access null sigs. - } + if (!Hardware.Registered) + { + return; // protects in cases where device has been unregistered and feedbacks would attempt to access null sigs. + } - foreach (var feedback in Feedbacks) - { - if (feedback != null) - feedback.FireUpdate(); - } + foreach (var feedback in Feedbacks) + { + if (feedback != null) + feedback.FireUpdate(); + } } #region IStatusMonitor Members @@ -195,52 +187,28 @@ namespace PepperDash.Essentials.Core public StatusMonitorBase CommunicationMonitor { get; private set; } #endregion - #region IUsageTracking Members + #region IUsageTracking Members - /// - /// Gets or sets the UsageTracker - /// - public UsageTracking UsageTracker { get; set; } + public UsageTracking UsageTracker { get; set; } - #endregion + #endregion } - /// - /// Abstract base class for Crestron GenericBase devices that are bridgeable - /// - public abstract class CrestronGenericBridgeableBaseDevice : CrestronGenericBaseDevice, IBridgeAdvanced +public abstract class CrestronGenericBridgeableBaseDevice : CrestronGenericBaseDevice, IBridgeAdvanced +{ + protected CrestronGenericBridgeableBaseDevice(string key, string name, GenericBase hardware) : base(key, name, hardware) { - - /// - /// Constructor - /// - /// key of the device - /// name of the device - /// hardware instance - protected CrestronGenericBridgeableBaseDevice(string key, string name, GenericBase hardware) : base(key, name, hardware) - { - } - - /// - /// Constructor without hardware instance - /// - /// key of the device - /// name of the device - protected CrestronGenericBridgeableBaseDevice(string key, string name) - : base(key, name) - { - } - - /// - /// Links to API - /// - /// the trilist - /// the starting join number - /// the join map key - /// the bridge instance - public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); } + protected CrestronGenericBridgeableBaseDevice(string key, string name) + : base(key, name) + { + } + + + public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); +} + //*********************************************************************************** /// @@ -281,5 +249,4 @@ namespace PepperDash.Essentials.Core return result; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/CrestronIO/GenericRelayDevice.cs b/src/PepperDash.Essentials.Core/CrestronIO/GenericRelayDevice.cs index a41e1411..15dfcd36 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/GenericRelayDevice.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/GenericRelayDevice.cs @@ -13,253 +13,209 @@ using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core.CrestronIO +namespace PepperDash.Essentials.Core.CrestronIO; + +/// +/// Represents a generic device controlled by relays +/// +[Description("Wrapper class for a Relay")] +public class GenericRelayDevice : EssentialsBridgeableDevice, ISwitchedOutput { - /// - /// Represents a generic device controlled by relays - /// - [Description("Wrapper class for a Relay")] - public class GenericRelayDevice : EssentialsBridgeableDevice, ISwitchedOutput + public Relay RelayOutput { get; private set; } + + public BoolFeedback OutputIsOnFeedback { get; private set; } + + //Maintained for compatibility with PepperDash.Essentials.Core.Devices.CrestronProcessor + public GenericRelayDevice(string key, Relay relay) : + base(key) { - /// - /// The RelayOutput controlled by this device - /// - public Relay RelayOutput { get; private set; } + OutputIsOnFeedback = new BoolFeedback(new Func(() => RelayOutput.State)); - /// - /// Feedback to indicate whether the output is on - /// - public BoolFeedback OutputIsOnFeedback { get; private set; } + RelayOutput = relay; + RelayOutput.Register(); - //Maintained for compatibility with PepperDash.Essentials.Core.Devices.CrestronProcessor - /// - /// Constructor for GenericRelayDevice - /// - /// key of the device - /// Relay output controlled by this device - public GenericRelayDevice(string key, Relay relay) : - base(key) + RelayOutput.StateChange += RelayOutput_StateChange; + } + + public GenericRelayDevice(string key, string name, Func postActivationFunc, + IOPortConfig config) + : base(key, name) + { + OutputIsOnFeedback = new BoolFeedback(() => RelayOutput.State); + + AddPostActivationAction(() => { - OutputIsOnFeedback = new BoolFeedback(new Func(() => RelayOutput.State)); - - RelayOutput = relay; - RelayOutput.Register(); - - RelayOutput.StateChange += RelayOutput_StateChange; - } - - /// - /// Constructor for GenericRelayDevice - /// - /// key of the device - /// name of the device - /// function to get the relay output - /// IO port configuration - public GenericRelayDevice(string key, string name, Func postActivationFunc, - IOPortConfig config) - : base(key, name) - { - OutputIsOnFeedback = new BoolFeedback(() => RelayOutput.State); - - AddPostActivationAction(() => - { - RelayOutput = postActivationFunc(config); - - if (RelayOutput == null) - { - Debug.LogMessage(LogEventLevel.Information, this, "Unable to get parent relay device for device key {0} and port {1}", config.PortDeviceKey, config.PortNumber); - return; - } - - RelayOutput.Register(); - - RelayOutput.StateChange += RelayOutput_StateChange; - }); - } - - #region PreActivate - - private static Relay GetRelay(IOPortConfig dc) - { - - IRelayPorts relayDevice; - - if(dc.PortDeviceKey.Equals("processor")) - { - if (!Global.ControlSystem.SupportsRelay) - { - Debug.LogMessage(LogEventLevel.Information, "Processor does not support relays"); - return null; - } - relayDevice = Global.ControlSystem; - - return relayDevice.RelayPorts[dc.PortNumber]; - } - - var essentialsDevice = DeviceManager.GetDeviceForKey(dc.PortDeviceKey); - if (essentialsDevice == null) - { - Debug.LogMessage(LogEventLevel.Information, "Device {0} was not found in Device Manager. Check configuration or for errors with device.", dc.PortDeviceKey); - return null; - } - - relayDevice = essentialsDevice as IRelayPorts; - - if (relayDevice == null) - { - Debug.LogMessage(LogEventLevel.Information, "Device {0} is not a valid relay parent. Please check configuration.", dc.PortDeviceKey); - return null; - } - - if (dc.PortNumber <= relayDevice.NumberOfRelayPorts) - { - return relayDevice.RelayPorts[dc.PortNumber]; - } - - Debug.LogMessage(LogEventLevel.Information, "Device {0} does not contain a port {1}", dc.PortDeviceKey, dc.PortNumber); - return null; - } - - #endregion - - #region Events - - void RelayOutput_StateChange(Relay relay, RelayEventArgs args) - { - OutputIsOnFeedback.FireUpdate(); - } - - #endregion - - #region Methods - - /// - /// OpenRelay method - /// - public void OpenRelay() - { - RelayOutput.State = false; - } - - /// - /// CloseRelay method - /// - public void CloseRelay() - { - RelayOutput.State = true; - } - - /// - /// ToggleRelayState method - /// - public void ToggleRelayState() - { - if (RelayOutput.State == true) - OpenRelay(); - else - CloseRelay(); - } - - #endregion - - #region ISwitchedOutput Members - - void ISwitchedOutput.On() - { - CloseRelay(); - } - - void ISwitchedOutput.Off() - { - OpenRelay(); - } - - #endregion - - #region Bridge Linking - - /// - /// LinkToApi method - /// - /// - public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - var joinMap = new GenericRelayControllerJoinMap(joinStart); - - var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); - - if (!string.IsNullOrEmpty(joinMapSerialized)) - joinMap = JsonConvert.DeserializeObject(joinMapSerialized); - - if (bridge != null) - { - bridge.AddJoinMap(Key, joinMap); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); - } + RelayOutput = postActivationFunc(config); if (RelayOutput == null) { - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to link device '{0}'. Relay is null", Key); + Debug.LogMessage(LogEventLevel.Information, this, "Unable to get parent relay device for device key {0} and port {1}", config.PortDeviceKey, config.PortNumber); return; } - Debug.LogMessage(LogEventLevel.Debug, this, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); - - trilist.SetBoolSigAction(joinMap.Relay.JoinNumber, b => - { - if (b) - CloseRelay(); - else - OpenRelay(); - }); - - // feedback for relay state - - OutputIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.Relay.JoinNumber]); - } - - #endregion - - #region Factory - - /// - /// Represents a GenericRelayDeviceFactory - /// - public class GenericRelayDeviceFactory : EssentialsDeviceFactory - { - /// - /// Constructor for GenericRelayDeviceFactory - /// - public GenericRelayDeviceFactory() - { - TypeNames = new List() { "relayoutput" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Relay Device"); - - var props = JsonConvert.DeserializeObject(dc.Properties.ToString()); - - if (props == null) return null; - - var portDevice = new GenericRelayDevice(dc.Key, dc.Name, GetRelay, props); - - return portDevice; - } - } - - #endregion - + RelayOutput.Register(); + RelayOutput.StateChange += RelayOutput_StateChange; + }); } + #region PreActivate + + private static Relay GetRelay(IOPortConfig dc) + { + + IRelayPorts relayDevice; + + if(dc.PortDeviceKey.Equals("processor")) + { + if (!Global.ControlSystem.SupportsRelay) + { + Debug.LogMessage(LogEventLevel.Information, "Processor does not support relays"); + return null; + } + relayDevice = Global.ControlSystem; + + return relayDevice.RelayPorts[dc.PortNumber]; + } + + var essentialsDevice = DeviceManager.GetDeviceForKey(dc.PortDeviceKey); + if (essentialsDevice == null) + { + Debug.LogMessage(LogEventLevel.Information, "Device {0} was not found in Device Manager. Check configuration or for errors with device.", dc.PortDeviceKey); + return null; + } + + relayDevice = essentialsDevice as IRelayPorts; + + if (relayDevice == null) + { + Debug.LogMessage(LogEventLevel.Information, "Device {0} is not a valid relay parent. Please check configuration.", dc.PortDeviceKey); + return null; + } + + if (dc.PortNumber <= relayDevice.NumberOfRelayPorts) + { + return relayDevice.RelayPorts[dc.PortNumber]; + } + + Debug.LogMessage(LogEventLevel.Information, "Device {0} does not contain a port {1}", dc.PortDeviceKey, dc.PortNumber); + return null; + } + + #endregion + + #region Events + + void RelayOutput_StateChange(Relay relay, RelayEventArgs args) + { + OutputIsOnFeedback.FireUpdate(); + } + + #endregion + + #region Methods + + public void OpenRelay() + { + RelayOutput.State = false; + } + + public void CloseRelay() + { + RelayOutput.State = true; + } + + public void ToggleRelayState() + { + if (RelayOutput.State == true) + OpenRelay(); + else + CloseRelay(); + } + + #endregion + + #region ISwitchedOutput Members + + void ISwitchedOutput.On() + { + CloseRelay(); + } + + void ISwitchedOutput.Off() + { + OpenRelay(); + } + + #endregion + + #region Bridge Linking + + public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + var joinMap = new GenericRelayControllerJoinMap(joinStart); + + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + + if (bridge != null) + { + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); + } + + if (RelayOutput == null) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to link device '{0}'. Relay is null", Key); + return; + } + + Debug.LogMessage(LogEventLevel.Debug, this, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); + + trilist.SetBoolSigAction(joinMap.Relay.JoinNumber, b => + { + if (b) + CloseRelay(); + else + OpenRelay(); + }); + + // feedback for relay state + + OutputIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.Relay.JoinNumber]); + } + + #endregion + + #region Factory + + public class GenericRelayDeviceFactory : EssentialsDeviceFactory + { + public GenericRelayDeviceFactory() + { + TypeNames = new List() { "relayoutput" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Relay Device"); + + var props = JsonConvert.DeserializeObject(dc.Properties.ToString()); + + if (props == null) return null; + + var portDevice = new GenericRelayDevice(dc.Key, dc.Name, GetRelay, props); + + return portDevice; + } + } + + #endregion + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportAnalogInputDevice.cs b/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportAnalogInputDevice.cs index ad5507d0..d9deb961 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportAnalogInputDevice.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportAnalogInputDevice.cs @@ -1,6 +1,4 @@ - - -using System; +using System; using System.Collections.Generic; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; diff --git a/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportInputDevice.cs b/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportInputDevice.cs index 47e8aef6..2088e952 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportInputDevice.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportInputDevice.cs @@ -1,6 +1,4 @@ - - -using System; +using System; using System.Collections.Generic; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; diff --git a/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportOutputDevice.cs b/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportOutputDevice.cs index ee3f0320..39c26cd6 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportOutputDevice.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportOutputDevice.cs @@ -11,173 +11,176 @@ using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core.CrestronIO +namespace PepperDash.Essentials.Core.CrestronIO; + +/// +/// Represents a generic digital input deviced tied to a versiport +/// +public class GenericVersiportDigitalOutputDevice : EssentialsBridgeableDevice, IDigitalOutput { - /// - /// Represents a generic digital input deviced tied to a versiport - /// - public class GenericVersiportDigitalOutputDevice : EssentialsBridgeableDevice, IDigitalOutput, IHasFeedback + public Versiport OutputPort { get; private set; } + + public BoolFeedback OutputStateFeedback { get; private set; } + + Func OutputStateFeedbackFunc { - private Versiport outputPort; - - /// - /// Gets or sets the OutputStateFeedback - /// - public BoolFeedback OutputStateFeedback { get; private set; } - - /// - public FeedbackCollection Feedbacks { get; private set; } = new FeedbackCollection(); - - /// - /// Initializes a new instance of the class. - /// - public GenericVersiportDigitalOutputDevice(string key, string name, Func postActivationFunc, IOPortConfig config) : - base(key, name) + get { - OutputStateFeedback = new BoolFeedback("outputState", () => outputPort.DigitalOut); - - AddPostActivationAction(() => - { - outputPort = postActivationFunc(config); - - outputPort.Register(); - - - if (!outputPort.SupportsDigitalOutput) - { - this.LogError("Device does not support configuration as a Digital Output"); - return; - } - - outputPort.SetVersiportConfiguration(eVersiportConfiguration.DigitalOutput); - - - outputPort.VersiportChange += OutputPort_VersiportChange; - - }); + return () => OutputPort.DigitalOut; } + } - void OutputPort_VersiportChange(Versiport port, VersiportEventArgs args) + public GenericVersiportDigitalOutputDevice(string key, string name, Func postActivationFunc, IOPortConfig config) : + base(key, name) + { + OutputStateFeedback = new BoolFeedback(OutputStateFeedbackFunc); + + AddPostActivationAction(() => { - this.LogDebug("Versiport change: {event}", args.Event); + OutputPort = postActivationFunc(config); - if (args.Event == eVersiportEvent.DigitalOutChange) - OutputStateFeedback.FireUpdate(); - } + OutputPort.Register(); - /// - /// Set value of the versiport digital output - /// - /// value to set the output to - public void SetOutput(bool state) - { - if (!outputPort.SupportsDigitalOutput) + + if (!OutputPort.SupportsDigitalOutput) { - this.LogError("Versiport does not support Digital Output Mode"); + Debug.LogMessage(LogEventLevel.Information, this, "Device does not support configuration as a Digital Output"); return; } - outputPort.DigitalOut = state; - } + OutputPort.SetVersiportConfiguration(eVersiportConfiguration.DigitalOutput); - #region Bridge Linking - /// - /// LinkToApi method - /// - /// - public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - var joinMap = new IDigitalOutputJoinMap(joinStart); + OutputPort.VersiportChange += OutputPort_VersiportChange; - var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + }); - if (!string.IsNullOrEmpty(joinMapSerialized)) - joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + } - if (bridge != null) + void OutputPort_VersiportChange(Versiport port, VersiportEventArgs args) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Versiport change: {0}", args.Event); + + if(args.Event == eVersiportEvent.DigitalOutChange) + OutputStateFeedback.FireUpdate(); + } + + /// + /// Set value of the versiport digital output + /// + /// value to set the output to + public void SetOutput(bool state) + { + if (OutputPort.SupportsDigitalOutput) { - bridge.AddJoinMap(Key, joinMap); + Debug.LogMessage(LogEventLevel.Information, this, "Passed the Check"); + + OutputPort.DigitalOut = state; + } else { - this.LogWarning("Please update config to use 'eiscapiadvanced' to get all join map features for this device."); + Debug.LogMessage(LogEventLevel.Information, this, "Versiport does not support Digital Output Mode"); } - try - { - this.LogDebug("Linking to Trilist '{0}'", trilist.ID.ToString("X")); + } - // Link feedback for input state - OutputStateFeedback.LinkInputSig(trilist.BooleanInput[joinMap.OutputState.JoinNumber]); - trilist.SetBoolSigAction(joinMap.OutputState.JoinNumber, SetOutput); - } - catch (Exception e) - { - this.LogError("Unable to link device: {message}", e.Message); - this.LogDebug(e, "Stack Trace: "); - } + #region Bridge Linking + + public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + var joinMap = new IDigitalOutputJoinMap(joinStart); + + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + + if (bridge != null) + { + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); } - #endregion - - - /// - /// GetVersiportDigitalOutput method - /// - public static Versiport GetVersiportDigitalOutput(IOPortConfig dc) + try { + Debug.LogMessage(LogEventLevel.Debug, this, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); + + // Link feedback for input state + OutputStateFeedback.LinkInputSig(trilist.BooleanInput[joinMap.OutputState.JoinNumber]); + trilist.SetBoolSigAction(joinMap.OutputState.JoinNumber, SetOutput); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to link device '{0}'. Input is null", Key); + Debug.LogMessage(LogEventLevel.Debug, this, "Error: {0}", e); + } + } + + #endregion + + + public static Versiport GetVersiportDigitalOutput(IOPortConfig dc) + { + + IIOPorts ioPortDevice; + if (dc.PortDeviceKey.Equals("processor")) { if (!Global.ControlSystem.SupportsVersiport) { - Debug.LogError("GetVersiportDigitalOutput: Processor does not support Versiports"); + Debug.LogMessage(LogEventLevel.Information, "GetVersiportDigitalOuptut: Processor does not support Versiports"); return null; } - return Global.ControlSystem.VersiPorts[dc.PortNumber]; + ioPortDevice = Global.ControlSystem; } - - if (!(DeviceManager.GetDeviceForKey(dc.PortDeviceKey) is IIOPorts ioPortDevice)) + else { - Debug.LogError("GetVersiportDigitalOutput: Device {key} is not a valid device", dc.PortDeviceKey); + var ioPortDev = DeviceManager.GetDeviceForKey(dc.PortDeviceKey) as IIOPorts; + if (ioPortDev == null) + { + Debug.LogMessage(LogEventLevel.Information, "GetVersiportDigitalOuptut: Device {0} is not a valid device", dc.PortDeviceKey); + return null; + } + ioPortDevice = ioPortDev; + } + if (ioPortDevice == null) + { + Debug.LogMessage(LogEventLevel.Information, "GetVersiportDigitalOuptut: Device '0' is not a valid IOPorts Device", dc.PortDeviceKey); return null; } if (dc.PortNumber > ioPortDevice.NumberOfVersiPorts) { - Debug.LogMessage(LogEventLevel.Information, "GetVersiportDigitalOutput: Device {0} does not contain a port {1}", dc.PortDeviceKey, dc.PortNumber); - return null; + Debug.LogMessage(LogEventLevel.Information, "GetVersiportDigitalOuptut: Device {0} does not contain a port {1}", dc.PortDeviceKey, dc.PortNumber); } - return ioPortDevice.VersiPorts[dc.PortNumber]; - } + var port = ioPortDevice.VersiPorts[dc.PortNumber]; + return port; + + } +} + + +public class GenericVersiportDigitalOutputDeviceFactory : EssentialsDeviceFactory +{ + public GenericVersiportDigitalOutputDeviceFactory() + { + TypeNames = new List() { "versiportoutput" }; } - - /// - /// Represents a GenericVersiportDigitalOutputDeviceFactory - /// - public class GenericVersiportDigitalOutputDeviceFactory : EssentialsDeviceFactory + public override EssentialsDevice BuildDevice(DeviceConfig dc) { - /// - /// Initialize a new instance of the class. - /// - public GenericVersiportDigitalOutputDeviceFactory() - { - TypeNames = new List() { "versiportoutput" }; - } + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Versiport Device"); - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogDebug("Factory Attempting to create new Generic Versiport Device"); + var props = JsonConvert.DeserializeObject(dc.Properties.ToString()); - var props = JsonConvert.DeserializeObject(dc.Properties.ToString()); + if (props == null) return null; - if (props == null) return null; + var portDevice = new GenericVersiportDigitalOutputDevice(dc.Key, dc.Name, GenericVersiportDigitalOutputDevice.GetVersiportDigitalOutput, props); - var portDevice = new GenericVersiportDigitalOutputDevice(dc.Key, dc.Name, GenericVersiportDigitalOutputDevice.GetVersiportDigitalOutput, props); - - return portDevice; - } + return portDevice; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/CrestronIO/IAnalogInput.cs b/src/PepperDash.Essentials.Core/CrestronIO/IAnalogInput.cs index 426834ed..c42e273c 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/IAnalogInput.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/IAnalogInput.cs @@ -5,7 +5,9 @@ using System.Text; using Crestron.SimplSharp; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.CrestronIO +namespace PepperDash.Essentials.Core.CrestronIO; + +public interface IAnalogInput { /// /// Defines the contract for IAnalogInput diff --git a/src/PepperDash.Essentials.Core/CrestronIO/IDigitalInput.cs b/src/PepperDash.Essentials.Core/CrestronIO/IDigitalInput.cs index 8e1b4c18..963fa8b1 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/IDigitalInput.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/IDigitalInput.cs @@ -1,19 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; + +namespace PepperDash.Essentials.Core.CrestronIO; -namespace PepperDash.Essentials.Core.CrestronIO + +/// +/// Represents a device that provides digital input +/// +public interface IDigitalInput { /// - /// Represents a device that provides digital input + /// Feedback to indicate the state of the input /// - public interface IDigitalInput - { - /// - /// Feedback to indicate the state of the input - /// - BoolFeedback InputStateFeedback { get; } - } -} \ No newline at end of file + BoolFeedback InputStateFeedback { get; } +} diff --git a/src/PepperDash.Essentials.Core/CrestronIO/IDigitalOutput.cs b/src/PepperDash.Essentials.Core/CrestronIO/IDigitalOutput.cs index 82aa0686..7c4f720a 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/IDigitalOutput.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/IDigitalOutput.cs @@ -4,7 +4,12 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core.CrestronIO +namespace PepperDash.Essentials.Core.CrestronIO; + +/// +/// Represents a device that provides digital input +/// +public interface IDigitalOutput { /// /// Represents a device that provides digital input diff --git a/src/PepperDash.Essentials.Core/CrestronIO/IHasCresnetBranches.cs b/src/PepperDash.Essentials.Core/CrestronIO/IHasCresnetBranches.cs index ee86b5fe..b882f50e 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/IHasCresnetBranches.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/IHasCresnetBranches.cs @@ -6,7 +6,12 @@ using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines the contract for a class that has Cresnet branches +/// +public interface IHasCresnetBranches { /// /// Defines the contract for IHasCresnetBranches diff --git a/src/PepperDash.Essentials.Core/CrestronIO/IOPortConfig.cs b/src/PepperDash.Essentials.Core/CrestronIO/IOPortConfig.cs index 7b76958b..f89b3b0f 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/IOPortConfig.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/IOPortConfig.cs @@ -1,5 +1,4 @@  - using System; using System.Collections.Generic; using System.Linq; diff --git a/src/PepperDash.Essentials.Core/CrestronIO/ISwitchedOutput.cs b/src/PepperDash.Essentials.Core/CrestronIO/ISwitchedOutput.cs index 30b4b8f9..9f5bec7b 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/ISwitchedOutput.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/ISwitchedOutput.cs @@ -6,37 +6,36 @@ using Crestron.SimplSharp; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.CrestronIO +namespace PepperDash.Essentials.Core.CrestronIO; + +/// +/// Describes an output capable of switching on and off +/// +public interface ISwitchedOutput { /// - /// Describes an output capable of switching on and off + /// Feedback to indicate the state of the output /// - public interface ISwitchedOutput - { - /// - /// Feedback to indicate whether the output is on - /// - BoolFeedback OutputIsOnFeedback {get;} - - /// - /// Turns the output on - /// - void On(); - - /// - /// Turns the output off - /// - void Off(); - } + BoolFeedback OutputIsOnFeedback {get;} /// - /// Describes a collection of switched outputs + /// Turns the output on /// - public interface ISwitchedOutputCollection - { - /// - /// Dictionary of switched outputs by their port number - /// - Dictionary SwitchedOutputs { get; } - } + void On(); + + /// + /// Turns the output off + /// + void Off(); +} + +/// +/// Defines the contract for a class that has a collection of switched outputs +/// +public interface ISwitchedOutputCollection +{ + /// + /// Collection of switched outputs, indexed by their output number + /// + Dictionary SwitchedOutputs { get; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Device Info/DeviceInfo.cs b/src/PepperDash.Essentials.Core/Device Info/DeviceInfo.cs index 6e55c9b3..abd011ef 100644 --- a/src/PepperDash.Essentials.Core/Device Info/DeviceInfo.cs +++ b/src/PepperDash.Essentials.Core/Device Info/DeviceInfo.cs @@ -1,29 +1,28 @@ -namespace PepperDash.Essentials.Core.DeviceInfo +namespace PepperDash.Essentials.Core.DeviceInfo; + +/// +/// Represents a DeviceInfo +/// +public class DeviceInfo { /// - /// Represents a DeviceInfo + /// Gets or sets the HostName /// - public class DeviceInfo - { - /// - /// Gets or sets the HostName - /// - public string HostName { get; set; } - /// - /// Gets or sets the IpAddress - /// - public string IpAddress { get; set; } - /// - /// Gets or sets the MacAddress - /// - public string MacAddress { get; set; } - /// - /// Gets or sets the SerialNumber - /// - public string SerialNumber { get; set; } - /// - /// Gets or sets the FirmwareVersion - /// - public string FirmwareVersion { get; set; } - } -} \ No newline at end of file + public string HostName { get; set; } + /// + /// Gets or sets the IpAddress + /// + public string IpAddress { get; set; } + /// + /// Gets or sets the MacAddress + /// + public string MacAddress { get; set; } + /// + /// Gets or sets the SerialNumber + /// + public string SerialNumber { get; set; } + /// + /// Gets or sets the FirmwareVersion + /// + public string FirmwareVersion { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Device Info/DeviceInfoEventArgs.cs b/src/PepperDash.Essentials.Core/Device Info/DeviceInfoEventArgs.cs index 266b0d0b..df4ea69d 100644 --- a/src/PepperDash.Essentials.Core/Device Info/DeviceInfoEventArgs.cs +++ b/src/PepperDash.Essentials.Core/Device Info/DeviceInfoEventArgs.cs @@ -1,32 +1,32 @@ using System; -namespace PepperDash.Essentials.Core.DeviceInfo +namespace PepperDash.Essentials.Core.DeviceInfo; + + +/// +/// Represents a DeviceInfoEventArgs +/// +public class DeviceInfoEventArgs : EventArgs { /// - /// Represents a DeviceInfoEventArgs + /// Gets or sets the DeviceInfo /// - public class DeviceInfoEventArgs:EventArgs + public DeviceInfo DeviceInfo { get; set; } + + /// + /// Constructor + /// + public DeviceInfoEventArgs() { - /// - /// Gets or sets the DeviceInfo - /// - public DeviceInfo DeviceInfo { get; set; } - /// - /// Constructor - /// - public DeviceInfoEventArgs() - { - - } - - /// - /// Constructor with DeviceInfo - /// - /// the DeviceInfo instance - public DeviceInfoEventArgs(DeviceInfo devInfo) - { - DeviceInfo = devInfo; - } } -} \ No newline at end of file + + /// + /// Constructor with DeviceInfo + /// + /// the DeviceInfo instance + public DeviceInfoEventArgs(DeviceInfo devInfo) + { + DeviceInfo = devInfo; + } +} diff --git a/src/PepperDash.Essentials.Core/Device Info/IDeviceInfoProvider.cs b/src/PepperDash.Essentials.Core/Device Info/IDeviceInfoProvider.cs index 3f5a4735..d4dc0852 100644 --- a/src/PepperDash.Essentials.Core/Device Info/IDeviceInfoProvider.cs +++ b/src/PepperDash.Essentials.Core/Device Info/IDeviceInfoProvider.cs @@ -1,31 +1,31 @@ using System; using PepperDash.Core; -namespace PepperDash.Essentials.Core.DeviceInfo +namespace PepperDash.Essentials.Core.DeviceInfo; + +/// +/// Defines the contract for IDeviceInfoProvider +/// +public interface IDeviceInfoProvider : IKeyed { /// - /// Defines the contract for IDeviceInfoProvider + /// Gets the DeviceInfo /// - public interface IDeviceInfoProvider:IKeyed - { - /// - /// Gets the DeviceInfo - /// - DeviceInfo DeviceInfo { get; } - - /// - /// Event fired when DeviceInfo changes - /// - event DeviceInfoChangeHandler DeviceInfoChanged; - - /// - /// Updates the DeviceInfo - /// - void UpdateDeviceInfo(); - } + DeviceInfo DeviceInfo { get; } /// - /// Delegate for DeviceInfoChangeHandler + /// Event fired when DeviceInfo changes /// - public delegate void DeviceInfoChangeHandler(IKeyed device, DeviceInfoEventArgs args); -} \ No newline at end of file + event DeviceInfoChangeHandler DeviceInfoChanged; + + /// + /// Updates the DeviceInfo + /// + void UpdateDeviceInfo(); +} + +/// +/// Delegate for DeviceInfoChangeHandler +/// +public delegate void DeviceInfoChangeHandler(IKeyed device, DeviceInfoEventArgs args); + diff --git a/src/PepperDash.Essentials.Core/Device Info/NetworkDeviceHelpers.cs b/src/PepperDash.Essentials.Core/Device Info/NetworkDeviceHelpers.cs index 79eaa018..14390754 100644 --- a/src/PepperDash.Essentials.Core/Device Info/NetworkDeviceHelpers.cs +++ b/src/PepperDash.Essentials.Core/Device Info/NetworkDeviceHelpers.cs @@ -6,233 +6,213 @@ using Crestron.SimplSharp; using PepperDash.Essentials.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.DeviceInfo +namespace PepperDash.Essentials.Core.DeviceInfo; + +public static class NetworkDeviceHelpers { /// - /// Static class NetworkDeviceHelpers + /// Event raised when ArpTable changes /// - public static class NetworkDeviceHelpers + public static event ArpTableEventHandler ArpTableUpdated; + + /// + /// Delegate called by ArpTableUpdated + /// + /// contains the entire ARP table and a bool to note if there was an error in retrieving the data + public delegate void ArpTableEventHandler(ArpTableEventArgs args); + + private static readonly char NewLineSplitter = CrestronEnvironment.NewLine.ToCharArray().First(); + private static readonly string NewLine = CrestronEnvironment.NewLine; + + private static readonly CCriticalSection Lock = new CCriticalSection(); + + /// + /// Last resolved ARP table - it is recommended to refresh the arp before using this. + /// + public static List ArpTable { get; private set; } + + /// + /// Force recheck of ARP table + /// + public static void RefreshArp() { - /// - /// Event raised when ArpTable changes - /// - public static event ArpTableEventHandler ArpTableUpdated; - - /// - /// Delegate called by ArpTableUpdated - /// - /// contains the entire ARP table and a bool to note if there was an error in retrieving the data - public delegate void ArpTableEventHandler(ArpTableEventArgs args); - - private static readonly char NewLineSplitter = CrestronEnvironment.NewLine.ToCharArray().First(); - private static readonly string NewLine = CrestronEnvironment.NewLine; - - private static readonly CCriticalSection Lock = new CCriticalSection(); - - /// - /// Gets or sets the ArpTable - /// - public static List ArpTable { get; private set; } - - /// - /// RefreshArp method - /// - public static void RefreshArp() + var error = false; + try { - var error = false; - try + Lock.Enter(); + var consoleResponse = string.Empty; + if (!CrestronConsole.SendControlSystemCommand("showarptable", ref consoleResponse)) return; + if (string.IsNullOrEmpty(consoleResponse)) { - Lock.Enter(); - var consoleResponse = string.Empty; - if (!CrestronConsole.SendControlSystemCommand("showarptable", ref consoleResponse)) return; - if (string.IsNullOrEmpty(consoleResponse)) - { - error = true; - return; - } - ArpTable.Clear(); - - Debug.LogMessage(LogEventLevel.Verbose, "ConsoleResponse of 'showarptable' : {0}{1}", NewLine, consoleResponse); - - var myLines = - consoleResponse.Split(NewLineSplitter) - .ToList() - .Where(o => (o.Contains(':') && !o.Contains("Type", StringComparison.OrdinalIgnoreCase))) - .ToList(); - foreach (var line in myLines) - { - var item = line; - var seperator = item.Contains('\t') ? '\t' : ' '; - var dataPoints = item.Split(seperator); - if (dataPoints == null || dataPoints.Length < 2) continue; - var ipAddress = SanitizeIpAddress(dataPoints.First().TrimAll()); - var macAddress = dataPoints.Last(); - ArpTable.Add(new ArpEntry(ipAddress, macAddress)); - } - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Information, "Exception in \"RefreshArp\" : {0}", ex.Message); error = true; + return; } - finally + ArpTable.Clear(); + + Debug.LogMessage(LogEventLevel.Verbose, "ConsoleResponse of 'showarptable' : {0}{1}", NewLine, consoleResponse); + + var myLines = + consoleResponse.Split(NewLineSplitter) + .ToList() + .Where(o => (o.Contains(':') && !o.Contains("Type", StringComparison.OrdinalIgnoreCase))) + .ToList(); + foreach (var line in myLines) { - Lock.Leave(); - OnArpTableUpdated(new ArpTableEventArgs(ArpTable, error)); + var item = line; + var seperator = item.Contains('\t') ? '\t' : ' '; + var dataPoints = item.Split(seperator); + if (dataPoints == null || dataPoints.Length < 2) continue; + var ipAddress = SanitizeIpAddress(dataPoints.First().TrimAll()); + var macAddress = dataPoints.Last(); + ArpTable.Add(new ArpEntry(ipAddress, macAddress)); } } - - - private static void OnArpTableUpdated(ArpTableEventArgs args) + catch (Exception ex) { - if (args == null) return; - var handler = ArpTableUpdated; - if (handler == null) return; - handler.Invoke(args); + Debug.LogMessage(LogEventLevel.Information, "Exception in \"RefreshArp\" : {0}", ex.Message); + error = true; } - - static NetworkDeviceHelpers() + finally { - ArpTable = new List(); + Lock.Leave(); + OnArpTableUpdated(new ArpTableEventArgs(ArpTable, error)); } + } - /// - /// Removes leading zeros, leading whitespace, and trailing whitespace from an IPAddress string - /// - /// Ip Address to Santitize - /// Sanitized Ip Address - /// - /// SanitizeIpAddress method - /// - public static string SanitizeIpAddress(string ipAddressIn) - { - try - { - var ipAddress = IPAddress.Parse(ipAddressIn.TrimStart('0')); - return ipAddress.ToString(); - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to Santize Ip : {0}", ex.Message); - return ipAddressIn; - } - } - /// - /// Resolves a hostname by IP Address using DNS - /// - /// IP Address to resolve from - /// Resolved Hostname - on failure to determine hostname, will return IP Address - /// - /// ResolveHostnameFromIp method - /// - public static string ResolveHostnameFromIp(string ipAddress) - { - try - { - var santitizedIp = SanitizeIpAddress(ipAddress); - var hostEntry = Dns.GetHostEntry(santitizedIp); - return hostEntry == null ? ipAddress : hostEntry.HostName; - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Information, "Exception Resolving Hostname from IP Address : {0}", ex.Message); - return ipAddress; - } - } - - /// - /// Resolves an IP Address by hostname using DNS - /// - /// Hostname to resolve from - /// Resolved IP Address - on a failure to determine IP Address, will return hostname - /// - /// ResolveIpFromHostname method - /// - public static string ResolveIpFromHostname(string hostName) - { - try - { - var hostEntry = Dns.GetHostEntry(hostName); - return hostEntry == null ? hostName : hostEntry.AddressList.First().ToString(); - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Information, "Exception Resolving IP Address from Hostname : {0}", ex.Message); - return hostName; - } - } + private static void OnArpTableUpdated(ArpTableEventArgs args) + { + if (args == null) return; + var handler = ArpTableUpdated; + if (handler == null) return; + handler.Invoke(args); + } + static NetworkDeviceHelpers() + { + ArpTable = new List(); } /// - /// Represents a ArpEntry + /// Removes leading zeros, leading whitespace, and trailing whitespace from an IPAddress string /// - public class ArpEntry + /// Ip Address to Santitize + /// Sanitized Ip Address + public static string SanitizeIpAddress(string ipAddressIn) { - /// - /// The IP Address of the ARP Entry - /// - public readonly IPAddress IpAddress; - - /// - /// The MAC Address of the ARP Entry - /// - public readonly string MacAddress; - - /// - /// Constructs new ArpEntry object - /// - /// string formatted as ipv4 address - /// mac address string - format is unimportant - public ArpEntry(string ipAddress, string macAddress) + try { - if (string.IsNullOrEmpty(ipAddress)) - { - throw new ArgumentException("\"ipAddress\" cannot be null or empty"); - } - if (string.IsNullOrEmpty(macAddress)) - { - throw new ArgumentException("\"macAddress\" cannot be null or empty"); - } - IpAddress = IPAddress.Parse(ipAddress.TrimStart().TrimStart('0').TrimEnd()); - MacAddress = macAddress; + var ipAddress = IPAddress.Parse(ipAddressIn.TrimStart('0')); + return ipAddress.ToString(); + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to Santize Ip : {0}", ex.Message); + return ipAddressIn; } } /// - /// Represents a ArpTableEventArgs + /// Resolves a hostname by IP Address using DNS /// - public class ArpTableEventArgs : EventArgs + /// IP Address to resolve from + /// Resolved Hostname - on failure to determine hostname, will return IP Address + public static string ResolveHostnameFromIp(string ipAddress) { - /// - /// The retrieved ARP Table - /// - public readonly List ArpTable; - /// - /// True if there was a problem retrieving the ARP Table - /// - public readonly bool Error; - - /// - /// Constructor for ArpTableEventArgs - /// - /// The entirety of the retrieved ARP table - /// True of an error was encountered updating the ARP table - public ArpTableEventArgs(List arpTable, bool error) + try { - ArpTable = arpTable; - Error = error; + var santitizedIp = SanitizeIpAddress(ipAddress); + var hostEntry = Dns.GetHostEntry(santitizedIp); + return hostEntry == null ? ipAddress : hostEntry.HostName; } - - /// - /// Constructor for ArpTableEventArgs - assumes no error encountered in retrieving ARP Table - /// - /// The entirety of the retrieved ARP table - public ArpTableEventArgs(List arpTable) + catch (Exception ex) { - ArpTable = arpTable; - Error = false; + Debug.LogMessage(LogEventLevel.Information, "Exception Resolving Hostname from IP Address : {0}", ex.Message); + return ipAddress; } } + + /// + /// Resolves an IP Address by hostname using DNS + /// + /// Hostname to resolve from + /// Resolved IP Address - on a failure to determine IP Address, will return hostname + public static string ResolveIpFromHostname(string hostName) + { + try + { + var hostEntry = Dns.GetHostEntry(hostName); + return hostEntry == null ? hostName : hostEntry.AddressList.First().ToString(); + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Information, "Exception Resolving IP Address from Hostname : {0}", ex.Message); + return hostName; + } + } + +} + +/// +/// Object to hold data about an arp entry +/// +public class ArpEntry +{ + public readonly IPAddress IpAddress; + public readonly string MacAddress; + + /// + /// Constructs new ArpEntry object + /// + /// string formatted as ipv4 address + /// mac address string - format is unimportant + public ArpEntry(string ipAddress, string macAddress) + { + if (string.IsNullOrEmpty(ipAddress)) + { + throw new ArgumentException("\"ipAddress\" cannot be null or empty"); + } + if (string.IsNullOrEmpty(macAddress)) + { + throw new ArgumentException("\"macAddress\" cannot be null or empty"); + } + IpAddress = IPAddress.Parse(ipAddress.TrimStart().TrimStart('0').TrimEnd()); + MacAddress = macAddress; + } +} + +/// +/// Arguments passed by the ArpTableUpdated event +/// +public class ArpTableEventArgs : EventArgs +{ + /// + /// The retrieved ARP Table + /// + public readonly List ArpTable; + /// + /// True if there was a problem retrieving the ARP Table + /// + public readonly bool Error; + + /// + /// Constructor for ArpTableEventArgs + /// + /// The entirety of the retrieved ARP table + /// True of an error was encountered updating the ARP table + public ArpTableEventArgs(List arpTable, bool error) + { + ArpTable = arpTable; + Error = error; + } + + /// + /// Constructor for ArpTableEventArgs - assumes no error encountered in retrieving ARP Table + /// + /// The entirety of the retrieved ARP table + public ArpTableEventArgs(List arpTable) + { + ArpTable = arpTable; + Error = false; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IChannel.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IChannel.cs index c8c54006..3dda43e4 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IChannel.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IChannel.cs @@ -4,8 +4,8 @@ using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.SmartObjects; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Defines the contract for IChannel /// @@ -79,5 +79,4 @@ namespace PepperDash.Essentials.Core triList.ClearBoolSigAction(129); triList.ClearBoolSigAction(134); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IColorFunctions.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IColorFunctions.cs index 69acc8f6..51f8e44a 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IColorFunctions.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IColorFunctions.cs @@ -4,8 +4,8 @@ using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.SmartObjects; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// IColor interface /// @@ -66,5 +66,4 @@ namespace PepperDash.Essentials.Core triList.ClearBoolSigAction(157); triList.ClearBoolSigAction(158); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDPad.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDPad.cs index ec9362b8..9e26cd5a 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDPad.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDPad.cs @@ -4,8 +4,8 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.SmartObjects; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// /// @@ -86,5 +86,4 @@ namespace PepperDash.Essentials.Core triList.ClearBoolSigAction(130); triList.ClearBoolSigAction(134); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDiscPlayerControls.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDiscPlayerControls.cs index 5deee690..b3d944e6 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDiscPlayerControls.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDiscPlayerControls.cs @@ -3,14 +3,11 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.SmartObjects; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; /// /// Defines the contract for IDiscPlayerControls /// public interface IDiscPlayerControls : IColor, IDPad, INumericKeypad, IHasPowerControl, ITransport, IUiDisplayInfo { - } - -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs index af3428f6..3f38d4e4 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs @@ -1,15 +1,14 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Interface for display devices that can be controlled and monitored. +/// This interface combines functionality for feedback, routing, power control, +/// warming/cooling, usage tracking, and key name management. +/// It is designed to be implemented by devices that require these capabilities, +/// such as projectors, displays, and other visual output devices. +/// +public interface IDisplay : IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName { - /// - /// Interface for display devices that can be controlled and monitored. - /// This interface combines functionality for feedback, routing, power control, - /// warming/cooling, usage tracking, and key name management. - /// It is designed to be implemented by devices that require these capabilities, - /// such as projectors, displays, and other visual output devices. - /// - public interface IDisplay : IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName - { - } } diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplayBasic.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplayBasic.cs index b76d3145..e744ac15 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplayBasic.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplayBasic.cs @@ -1,14 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; - namespace PepperDash.Essentials.Core.Devices.DeviceTypeInterfaces { /// /// Defines the contract for IDisplayBasic /// + [Obsolete("This interface is no longer used and will be removed in a future version. Please use IDisplay instead.")] public interface IDisplayBasic { /// diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDumbSource.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDumbSource.cs index 2f419498..ba7f1a43 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDumbSource.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDumbSource.cs @@ -4,12 +4,11 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines the contract for IDumbSource +/// +public interface IDumbSource { - /// - /// Defines the contract for IDumbSource - /// - public interface IDumbSource - { - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDvr.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDvr.cs index 527e8d46..4397db50 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDvr.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDvr.cs @@ -9,8 +9,8 @@ using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.SmartObjects; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// /// @@ -55,5 +55,4 @@ namespace PepperDash.Essentials.Core triList.ClearBoolSigAction(136); triList.ClearBoolSigAction(152); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IEmergencyOSD.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IEmergencyOSD.cs index efc068b0..66d74c3e 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IEmergencyOSD.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IEmergencyOSD.cs @@ -1,25 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces + +/// +/// Defines the contract for IEmergencyOSD +/// +public interface IEmergencyOSD { /// - /// Defines the contract for IEmergencyOSD + /// Shows an emergency message on the OSD /// - public interface IEmergencyOSD - { - /// - /// Shows an emergency message on the OSD - /// - /// The URL of the emergency message to display - void ShowEmergencyMessage(string url); + /// The URL of the emergency message to display + void ShowEmergencyMessage(string url); - /// - /// Hides the emergency message from the OSD - /// - void HideEmergencyMessage(); - } + /// + /// Hides the emergency message from the OSD + /// + void HideEmergencyMessage(); } + diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasBranding.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasBranding.cs index c05ef572..75f04156 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasBranding.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasBranding.cs @@ -1,21 +1,18 @@ -using System; +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +/// +/// Defines the contract for IHasBranding +/// +public interface IHasBranding { /// - /// Defines the contract for IHasBranding + /// Gets whether branding is enabled /// - public interface IHasBranding - { - /// - /// Gets whether branding is enabled - /// - bool BrandingEnabled { get; } + bool BrandingEnabled { get; } - /// - /// Initializes branding for the device - /// - /// The key identifying the room for branding purposes - void InitializeBranding(string roomKey); - } -} \ No newline at end of file + /// + /// Initializes branding for the device + /// + /// The key identifying the room for branding purposes + void InitializeBranding(string roomKey); +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasFarEndContentStatus.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasFarEndContentStatus.cs index 5a8f66ee..1595fa6e 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasFarEndContentStatus.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasFarEndContentStatus.cs @@ -1,13 +1,13 @@ -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +/// +/// Defines the contract for IHasFarEndContentStatus +/// +public interface IHasFarEndContentStatus { /// - /// Defines the contract for IHasFarEndContentStatus + /// Gets whether far end content is being received /// - public interface IHasFarEndContentStatus - { - /// - /// Gets whether far end content is being received - /// - BoolFeedback ReceivingContent { get; } - } -} \ No newline at end of file + BoolFeedback ReceivingContent { get; } +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasInputs.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasInputs.cs index 42e97b4b..d6fae39d 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasInputs.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasInputs.cs @@ -1,19 +1,20 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +/// +/// Describes a device that has selectable inputs +/// +/// the type to use as the key for each input item. Most likely an enum or string\ +/// +/// See MockDisplay for example implemntation +/// +public interface IHasInputs : IKeyName { /// - /// Describes a device that has selectable inputs + /// Gets the collection of inputs /// - /// the type to use as the key for each input item. Most likely an enum or string\ - /// - /// See MockDisplay for example implemntation - /// - public interface IHasInputs : IKeyName - { - /// - /// Gets the collection of inputs - /// - ISelectableItems Inputs { get; } - } + ISelectableItems Inputs { get; } } + diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasPhoneDialing.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasPhoneDialing.cs index b884092b..170b058d 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasPhoneDialing.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasPhoneDialing.cs @@ -1,43 +1,40 @@ -using System; -using PepperDash.Essentials.Core; +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces + +/// +/// Defines the contract for IHasPhoneDialing +/// +public interface IHasPhoneDialing { /// - /// Defines the contract for IHasPhoneDialing + /// Feedback that indicates whether the phone is off-hook /// - public interface IHasPhoneDialing - { - /// - /// Feedback that indicates whether the phone is off-hook - /// - BoolFeedback PhoneOffHookFeedback { get; } + BoolFeedback PhoneOffHookFeedback { get; } - /// - /// Feedback that provides the caller ID name - /// - StringFeedback CallerIdNameFeedback { get; } + /// + /// Feedback that provides the caller ID name + /// + StringFeedback CallerIdNameFeedback { get; } - /// - /// Feedback that provides the caller ID number - /// - StringFeedback CallerIdNumberFeedback { get; } + /// + /// Feedback that provides the caller ID number + /// + StringFeedback CallerIdNumberFeedback { get; } - /// - /// Dials a phone call to the specified number - /// - /// the number to dial - void DialPhoneCall(string number); + /// + /// Dials a phone call to the specified number + /// + /// the number to dial + void DialPhoneCall(string number); - /// - /// Ends the current phone call - /// - void EndPhoneCall(); + /// + /// Ends the current phone call + /// + void EndPhoneCall(); - /// - /// Sends a DTMF digit to the phone - /// - /// the DTMF digit to send - void SendDtmfToPhone(string digit); - } -} \ No newline at end of file + /// + /// Sends a DTMF digit to the phone + /// + /// the DTMF digit to send + void SendDtmfToPhone(string digit); +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasScreensWithLayouts.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasScreensWithLayouts.cs index 439462c6..2a67e39b 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasScreensWithLayouts.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasScreensWithLayouts.cs @@ -5,103 +5,102 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// This defines a device that has screens with layouts +/// Simply decorative +/// +public interface IHasScreensWithLayouts { /// - /// This defines a device that has screens with layouts - /// Simply decorative + /// A dictionary of screens, keyed by screen ID, that contains information about each screen and its layouts. /// - public interface IHasScreensWithLayouts - { - /// - /// A dictionary of screens, keyed by screen ID, that contains information about each screen and its layouts. - /// - Dictionary Screens { get; } - - /// - /// Applies a specific layout to a screen based on the provided screen ID and layout index. - /// - /// - /// - void ApplyLayout(uint screenId, uint layoutIndex); - } + Dictionary Screens { get; } /// - /// Represents information about a screen and its layouts. + /// Applies a specific layout to a screen based on the provided screen ID and layout index. /// - public class ScreenInfo - { - - /// - /// Indicates whether the screen is enabled or not. - /// - [JsonProperty("enabled")] - public bool Enabled { get; set; } - - /// - /// The name of the screen. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// The index of the screen. - /// - [JsonProperty("screenIndex")] - public int ScreenIndex { get; set; } - - /// - /// A dictionary of layout information for the screen, keyed by layout ID. - /// - [JsonProperty("layouts")] - public Dictionary Layouts { get; set; } - } - - /// - /// Represents information about a layout on a screen. - /// - public class LayoutInfo - { - /// - /// The name of the layout. - /// - [JsonProperty("layoutName")] - public string LayoutName { get; set; } - - /// - /// The index of the layout. - /// - [JsonProperty("layoutIndex")] - public int LayoutIndex { get; set; } - - /// - /// The type of the layout, which can be "single", "double", "triple", or "quad". - /// - [JsonProperty("layoutType")] - public string LayoutType { get; set; } - - /// - /// A dictionary of window configurations for the layout, keyed by window ID. - /// - [JsonProperty("windows")] - public Dictionary Windows { get; set; } - } - - /// - /// Represents the configuration of a window within a layout on a screen. - /// - public class WindowConfig - { - /// - /// The display label for the window - /// - [JsonProperty("label")] - public string Label { get; set; } - - /// - /// The input for the window - /// - [JsonProperty("input")] - public string Input { get; set; } - } + /// + /// + void ApplyLayout(uint screenId, uint layoutIndex); +} + +/// +/// Represents information about a screen and its layouts. +/// +public class ScreenInfo +{ + + /// + /// Indicates whether the screen is enabled or not. + /// + [JsonProperty("enabled")] + public bool Enabled { get; set; } + + /// + /// The name of the screen. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The index of the screen. + /// + [JsonProperty("screenIndex")] + public int ScreenIndex { get; set; } + + /// + /// A dictionary of layout information for the screen, keyed by layout ID. + /// + [JsonProperty("layouts")] + public Dictionary Layouts { get; set; } +} + +/// +/// Represents information about a layout on a screen. +/// +public class LayoutInfo +{ + /// + /// The name of the layout. + /// + [JsonProperty("layoutName")] + public string LayoutName { get; set; } + + /// + /// The index of the layout. + /// + [JsonProperty("layoutIndex")] + public int LayoutIndex { get; set; } + + /// + /// The type of the layout, which can be "single", "double", "triple", or "quad". + /// + [JsonProperty("layoutType")] + public string LayoutType { get; set; } + + /// + /// A dictionary of window configurations for the layout, keyed by window ID. + /// + [JsonProperty("windows")] + public Dictionary Windows { get; set; } +} + +/// +/// Represents the configuration of a window within a layout on a screen. +/// +public class WindowConfig +{ + /// + /// The display label for the window + /// + [JsonProperty("label")] + public string Label { get; set; } + + /// + /// The input for the window + /// + [JsonProperty("input")] + public string Input { get; set; } } diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasSurroundSoundModes.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasSurroundSoundModes.cs index 45b24f8b..2a50dd0f 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasSurroundSoundModes.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasSurroundSoundModes.cs @@ -1,9 +1,4 @@ using PepperDash.Core; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials.Core.DeviceTypeInterfaces { diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWebView.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWebView.cs index 396229ce..d1ae55bd 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWebView.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWebView.cs @@ -1,87 +1,84 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +/// +/// Defines the contract for IHasWebView +/// +public interface IHasWebView { /// - /// Defines the contract for IHasWebView + /// Indicates whether the webview is currently visible /// - public interface IHasWebView - { - /// - /// Indicates whether the webview is currently visible - /// - bool WebviewIsVisible { get; } - - /// - /// Shows the webview with the specified parameters - /// - /// the URL to display in the webview - /// the display mode for the webview - /// the title to display on the webview - /// the target for the webview - void ShowWebView(string url, string mode, string title, string target); - - /// - /// Hides the webview - /// - void HideWebView(); - - /// - /// Event raised when the webview status changes - /// - event EventHandler WebViewStatusChanged; - } - + bool WebviewIsVisible { get; } /// - /// Defines the contract for IHasWebViewWithPwaMode + /// Shows the webview with the specified parameters /// - public interface IHasWebViewWithPwaMode : IHasWebView - { - /// - /// Indicates whether the webview is currently in PWA mode - /// - bool IsInPwaMode { get; } - - /// - /// Gets the BoolFeedback indicating whether the webview is currently in PWA mode - /// - BoolFeedback IsInPwaModeFeedback { get; } - - /// - /// Sends navigators to the specified PWA URL. Accepts an absolute URL or a relative URL for a mobile control app - /// - /// The URL to navigate to - void SendNavigatorsToPwaUrl(string url); - - /// - /// Exits navigators from PWA mode - /// - void ExitNavigatorsPwaMode(); - } - + /// the URL to display in the webview + /// the display mode for the webview + /// the title to display on the webview + /// the target for the webview + void ShowWebView(string url, string mode, string title, string target); /// - /// Represents a WebViewStatusChangedEventArgs + /// Hides the webview /// - public class WebViewStatusChangedEventArgs : EventArgs - { - /// - /// Gets or sets the Status - /// - public string Status { get; } + void HideWebView(); - /// - /// Constructor for WebViewStatusChangedEventArgs - /// - /// the new status of the webview - public WebViewStatusChangedEventArgs(string status) - { - Status = status; - } + /// + /// Event raised when the webview status changes + /// + event EventHandler WebViewStatusChanged; +} + + +/// +/// Defines the contract for IHasWebViewWithPwaMode +/// +public interface IHasWebViewWithPwaMode : IHasWebView +{ + /// + /// Indicates whether the webview is currently in PWA mode + /// + bool IsInPwaMode { get; } + + /// + /// Gets the BoolFeedback indicating whether the webview is currently in PWA mode + /// + BoolFeedback IsInPwaModeFeedback { get; } + + /// + /// Sends navigators to the specified PWA URL. Accepts an absolute URL or a relative URL for a mobile control app + /// + /// The URL to navigate to + void SendNavigatorsToPwaUrl(string url); + + /// + /// Exits navigators from PWA mode + /// + void ExitNavigatorsPwaMode(); +} + + +/// +/// Represents a WebViewStatusChangedEventArgs +/// +public class WebViewStatusChangedEventArgs : EventArgs +{ + /// + /// Gets or sets the Status + /// + public string Status { get; } + + /// + /// Constructor for WebViewStatusChangedEventArgs + /// + /// the new status of the webview + public WebViewStatusChangedEventArgs(string status) + { + Status = status; } } + diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHumiditySensor.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHumiditySensor.cs index 748fb860..f28059c3 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHumiditySensor.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHumiditySensor.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - + namespace PepperDash.Essentials.Core.DeviceTypeInterfaces { /// diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILanguageDefinition.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILanguageDefinition.cs index f02c0e05..4cdffc83 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILanguageDefinition.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILanguageDefinition.cs @@ -1,56 +1,57 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +/// +/// Defines the contract for ILanguageDefinition +/// +public interface ILanguageDefinition { /// - /// Defines the contract for ILanguageDefinition + /// The locale name for the language definition /// - public interface ILanguageDefinition - { - /// - /// The locale name for the language definition - /// - string LocaleName { get; set; } + string LocaleName { get; set; } - /// - /// The friendly name for the language definition - /// - string FriendlyName { get; set; } + /// + /// The friendly name for the language definition + /// + string FriendlyName { get; set; } - /// - /// Indicates whether the language definition is enabled - /// - bool Enable { get; set; } + /// + /// Indicates whether the language definition is enabled + /// + bool Enable { get; set; } - /// - /// The UI labels for the language definition - /// - List UiLabels { get; set; } + /// + /// The UI labels for the language definition + /// + List UiLabels { get; set; } - /// - /// The source and destination labels for the language definition - /// - List Sources { get; set; } + /// + /// The source and destination labels for the language definition + /// + List Sources { get; set; } - /// - /// The destination labels for the language definition - /// - List Destinations { get; set; } + /// + /// The destination labels for the language definition + /// + List Destinations { get; set; } - /// - /// The source group names for the language definition - /// - List SourceGroupNames { get; set; } + /// + /// The source group names for the language definition + /// + List SourceGroupNames { get; set; } - /// - /// The destination group names for the language definition - /// - List DestinationGroupNames { get; set; } + /// + /// The destination group names for the language definition + /// + List DestinationGroupNames { get; set; } - /// - /// The room names for the language definition - /// - List RoomNames { get; set; } - } + /// + /// The room names for the language definition + /// + List RoomNames { get; set; } } + diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILanguageProvider.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILanguageProvider.cs index 9ac22d2b..ac9544ea 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILanguageProvider.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILanguageProvider.cs @@ -1,22 +1,21 @@ using System; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Defines the contract for ILanguageProvider +/// +public interface ILanguageProvider { - /// - /// Defines the contract for ILanguageProvider + /// The current language definition /// - public interface ILanguageProvider - { - /// - /// The current language definition - /// - ILanguageDefinition CurrentLanguage { get; set; } - - /// - /// Event raised when the current language changes - /// - event EventHandler CurrentLanguageChanged; - } + ILanguageDefinition CurrentLanguage { get; set; } + /// + /// Event raised when the current language changes + /// + event EventHandler CurrentLanguageChanged; } + + diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILevelControls.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILevelControls.cs index e0a1081f..d4876e3c 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILevelControls.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ILevelControls.cs @@ -4,16 +4,17 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +/// +/// Defines the contract for ILevelControls +/// +public interface ILevelControls { /// - /// Defines the contract for ILevelControls + /// The level control points /// - public interface ILevelControls - { - /// - /// The level control points - /// - Dictionary LevelControlPoints { get; } - } + Dictionary LevelControlPoints { get; } } + diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControl.cs index 0b1760fa..f920e5fe 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControl.cs @@ -2,71 +2,70 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +/// +/// Defines the contract for IMobileControl +/// +public interface IMobileControl : IKeyed { + /// + /// Gets the Host + /// + string Host { get; } /// - /// Defines the contract for IMobileControl + /// Gets the Client App URL /// - public interface IMobileControl : IKeyed - { - /// - /// Gets the Host - /// - string Host { get; } + string ClientAppUrl { get; } - /// - /// Gets the Client App URL - /// - string ClientAppUrl { get; } + /// + /// Gets the System UUID + /// + string SystemUuid { get; } - /// - /// Gets the System UUID - /// - string SystemUuid { get; } + /// + /// Gets the ApiOnlineAndAuthorized feedback + /// + BoolFeedback ApiOnlineAndAuthorized { get; } - /// - /// Gets the ApiOnlineAndAuthorized feedback - /// - BoolFeedback ApiOnlineAndAuthorized { get; } + /// + /// Sends the message object to the AppServer + /// + /// Message to send + void SendMessageObject(IMobileControlMessage o); - /// - /// Sends the message object to the AppServer - /// - /// Message to send - void SendMessageObject(IMobileControlMessage o); + /// + /// Adds an action for a messenger + /// + /// Messenger type. Must implement IMobileControlMessenger + /// messenger to register + /// action to add + void AddAction(T messenger, Action action) where T : IMobileControlMessenger; - /// - /// Adds an action for a messenger - /// - /// Messenger type. Must implement IMobileControlMessenger - /// messenger to register - /// action to add - void AddAction(T messenger, Action action) where T : IMobileControlMessenger; + /// + /// Removes an action for a messenger + /// + /// key for action + void RemoveAction(string key); - /// - /// Removes an action for a messenger - /// - /// key for action - void RemoveAction(string key); + /// + /// Adds a device messenger + /// + /// Messenger to add + void AddDeviceMessenger(IMobileControlMessenger messenger); - /// - /// Adds a device messenger - /// - /// Messenger to add - void AddDeviceMessenger(IMobileControlMessenger messenger); + /// + /// Check if a device messenger exists + /// + /// Messenger key to find + bool CheckForDeviceMessenger(string key); - /// - /// Check if a device messenger exists - /// - /// Messenger key to find - bool CheckForDeviceMessenger(string key); - - /// - /// Get a Room Messenger by key - /// - /// messenger key to find - /// Messenger if found, null otherwise - IMobileControlRoomMessenger GetRoomMessenger(string key); - } -} \ No newline at end of file + /// + /// Get a Room Messenger by key + /// + /// messenger key to find + /// Messenger if found, null otherwise + IMobileControlRoomMessenger GetRoomMessenger(string key); +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IPasswordPrompt.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IPasswordPrompt.cs index 2ee85341..a068ba19 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IPasswordPrompt.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IPasswordPrompt.cs @@ -4,63 +4,62 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Describes the functionality required to prompt a user to enter a password +/// +public interface IPasswordPrompt { /// - /// Describes the functionality required to prompt a user to enter a password + /// Notifies when a password is required or is entered incorrectly /// - public interface IPasswordPrompt - { - /// - /// Notifies when a password is required or is entered incorrectly - /// - event EventHandler PasswordRequired; - - /// - /// Submits the password - /// - /// The password to submit - void SubmitPassword(string password); - } + event EventHandler PasswordRequired; /// - /// PasswordPromptEventArgs class + /// Submits the password /// - public class PasswordPromptEventArgs : EventArgs + /// + void SubmitPassword(string password); +} + +/// +/// Event args for password prompt events +/// +public class PasswordPromptEventArgs : EventArgs +{ + /// + /// Indicates if the last submitted password was incorrect + /// + public bool LastAttemptWasIncorrect { get; private set; } + + /// + /// Indicates that the login attempt has failed + /// + public bool LoginAttemptFailed { get; private set; } + + /// + /// Indicates that the process was cancelled and the prompt should be dismissed + /// + public bool LoginAttemptCancelled { get; private set; } + + /// + /// A message to be displayed to the user + /// + public string Message { get; private set; } + + /// + /// Constructor for password prompt event args + /// + /// Indicates if the last submitted password was incorrect + /// Indicates that the login attempt has failed + /// Indicates that the process was cancelled and the prompt should be dismissed + /// A message to be displayed to the user + public PasswordPromptEventArgs(bool lastAttemptIncorrect, bool loginFailed, bool loginCancelled, string message) { - /// - /// Indicates if the last submitted password was incorrect - /// - public bool LastAttemptWasIncorrect { get; private set; } - - /// - /// Gets or sets the LoginAttemptFailed - /// - public bool LoginAttemptFailed { get; private set; } - - /// - /// Gets or sets the LoginAttemptCancelled - /// - public bool LoginAttemptCancelled { get; private set; } - - /// - /// Gets or sets the Message - /// - public string Message { get; private set; } - - /// - /// Constructor - /// - /// indicates if the last submitted password was incorrect - /// indicates if the login attempt failed - /// indicates if the login attempt was cancelled - /// provides a message related to the password prompt - public PasswordPromptEventArgs(bool lastAttemptIncorrect, bool loginFailed, bool loginCancelled, string message) - { - LastAttemptWasIncorrect = lastAttemptIncorrect; - LoginAttemptFailed = loginFailed; - LoginAttemptCancelled = loginCancelled; - Message = message; - } + LastAttemptWasIncorrect = lastAttemptIncorrect; + LoginAttemptFailed = loginFailed; + LoginAttemptCancelled = loginCancelled; + Message = message; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IPower.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IPower.cs index b94a64ed..a94dfe12 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IPower.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IPower.cs @@ -1,86 +1,77 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro; -using Crestron.SimplSharpPro.DeviceSupport; -using Crestron.SimplSharpPro.Fusion; +using Crestron.SimplSharpPro.DeviceSupport; +namespace PepperDash.Essentials.Core; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.SmartObjects; +/// +/// Adds feedback for current power state +/// +public interface IHasPowerControlWithFeedback : IHasPowerControl +{ + /// + /// Feedback indicating whether the device is powered on + /// + BoolFeedback PowerIsOnFeedback { get; } +} -namespace PepperDash.Essentials.Core + +/// +/// Defines the ability to power a device on and off +/// +public interface IHasPowerControl +{ + /// + /// Powers the device on + /// + void PowerOn(); + + /// + /// Powers the device off + /// + void PowerOff(); + + /// + /// Toggles the power state of the device + /// + void PowerToggle(); +} + + + +/// +/// IHasPowerControlExtensions class +/// +public static class IHasPowerControlExtensions { /// - /// Adds feedback for current power state + /// LinkButtons method /// - public interface IHasPowerControlWithFeedback : IHasPowerControl + public static void LinkButtons(this IHasPowerControl dev, BasicTriList triList) { - /// - /// Feedback indicating whether the device is powered on - /// - BoolFeedback PowerIsOnFeedback { get; } + triList.SetSigFalseAction(101, dev.PowerOn); + triList.SetSigFalseAction(102, dev.PowerOff); + triList.SetSigFalseAction(103, dev.PowerToggle); + + var fbdev = dev as IHasPowerControlWithFeedback; + if (fbdev != null) + { + fbdev.PowerIsOnFeedback.LinkInputSig(triList.BooleanInput[101]); + } } /// - /// Defines the ability to power a device on and off + /// UnlinkButtons method /// - public interface IHasPowerControl + public static void UnlinkButtons(this IHasPowerControl dev, BasicTriList triList) { - /// - /// Powers the device on - /// - void PowerOn(); + triList.ClearBoolSigAction(101); + triList.ClearBoolSigAction(102); + triList.ClearBoolSigAction(103); - /// - /// Powers the device off - /// - void PowerOff(); - - /// - /// Toggles the power state of the device - /// - void PowerToggle(); + var fbdev = dev as IHasPowerControlWithFeedback; + if (fbdev != null) + { + fbdev.PowerIsOnFeedback.UnlinkInputSig(triList.BooleanInput[101]); + } } - - /// - /// IHasPowerControlExtensions class - /// - public static class IHasPowerControlExtensions - { - /// - /// LinkButtons method - /// - public static void LinkButtons(this IHasPowerControl dev, BasicTriList triList) - { - triList.SetSigFalseAction(101, dev.PowerOn); - triList.SetSigFalseAction(102, dev.PowerOff); - triList.SetSigFalseAction(103, dev.PowerToggle); - - var fbdev = dev as IHasPowerControlWithFeedback; - if (fbdev != null) - { - fbdev.PowerIsOnFeedback.LinkInputSig(triList.BooleanInput[101]); - } - } - - /// - /// UnlinkButtons method - /// - public static void UnlinkButtons(this IHasPowerControl dev, BasicTriList triList) - { - triList.ClearBoolSigAction(101); - triList.ClearBoolSigAction(102); - triList.ClearBoolSigAction(103); - - var fbdev = dev as IHasPowerControlWithFeedback; - if (fbdev != null) - { - fbdev.PowerIsOnFeedback.UnlinkInputSig(triList.BooleanInput[101]); - } - } - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IProjectorScreenLiftControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IProjectorScreenLiftControl.cs index 56aef09e..efa3a0c4 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IProjectorScreenLiftControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IProjectorScreenLiftControl.cs @@ -3,8 +3,8 @@ using System; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces -{ +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + /// /// Defines the contract for IProjectorScreenLiftControl /// @@ -60,5 +60,4 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces /// Screen type device /// screen - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISelectableItem.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISelectableItem.cs index ff05188e..410cf563 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISelectableItem.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISelectableItem.cs @@ -2,28 +2,28 @@ using Newtonsoft.Json; using PepperDash.Core; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + + +/// +/// Defines the contract for ISelectableItem +/// +public interface ISelectableItem : IKeyName { + /// + /// Raised when the item is updated + /// + event EventHandler ItemUpdated; /// - /// Defines the contract for ISelectableItem + /// Gets or sets whether the item is selected /// - public interface ISelectableItem : IKeyName - { - /// - /// Raised when the item is updated - /// - event EventHandler ItemUpdated; + [JsonProperty("isSelected")] + bool IsSelected { get; set; } - /// - /// Gets or sets whether the item is selected - /// - [JsonProperty("isSelected")] - bool IsSelected { get; set; } - - /// - /// Selects the item - /// - void Select(); - } -} \ No newline at end of file + /// + /// Selects the item + /// + void Select(); +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISelectableItems.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISelectableItems.cs index c955a56f..94a03a51 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISelectableItems.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISelectableItems.cs @@ -2,42 +2,41 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +/// +/// Defines the contract for ISelectableItems +/// +public interface ISelectableItems where TValue : ISelectableItem { /// - /// Defines the contract for ISelectableItems + /// Raised when the items are updated /// - public interface ISelectableItems where TValue : ISelectableItem - { - /// - /// Raised when the items are updated - /// - event EventHandler ItemsUpdated; - - /// - /// Raised when the current item changes - /// - event EventHandler CurrentItemChanged; - - /// - /// Gets or sets the collection of selectable items - /// - [JsonProperty("items")] - Dictionary Items { get; set; } - - /// - /// Gets or sets the current selected item key - /// - [JsonProperty("currentItem")] - TKey CurrentItem { get; set; } - - } + event EventHandler ItemsUpdated; /// - /// Describes a collection of items that can be selected + /// Raised when the current item changes /// - /// type for the keys in the collection. Probably a string or enum - public interface ISelectableItems : ISelectableItems - { - } + event EventHandler CurrentItemChanged; + + /// + /// Gets or sets the collection of selectable items + /// + [JsonProperty("items")] + Dictionary Items { get; set; } + + /// + /// Gets or sets the current selected item key + /// + [JsonProperty("currentItem")] + TKey CurrentItem { get; set; } +} + +/// +/// Describes a collection of items that can be selected +/// +/// type for the keys in the collection. Probably a string or enum +public interface ISelectableItems : ISelectableItems +{ } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISetTopBoxControls.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISetTopBoxControls.cs index d8bf969e..e195f16f 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISetTopBoxControls.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ISetTopBoxControls.cs @@ -3,8 +3,8 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.SmartObjects; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Defines the contract for ISetTopBoxControls /// @@ -81,5 +81,4 @@ namespace PepperDash.Essentials.Core triList.ClearBoolSigAction(136); triList.ClearBoolSigAction(152); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITemperatureSensor.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITemperatureSensor.cs index 897855b4..e6b09f8b 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITemperatureSensor.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITemperatureSensor.cs @@ -1,31 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces + +/// +/// Defines the contract for ITemperatureSensor +/// +public interface ITemperatureSensor { /// - /// Defines the contract for ITemperatureSensor + /// The values will range from -400 to +1760 (for -40° to +176° F) or -400 to +800 + /// (for -40° to +80° C)in tenths of a degree. /// - public interface ITemperatureSensor - { - /// - /// The values will range from -400 to +1760 (for -40° to +176° F) or -400 to +800 - /// (for -40° to +80° C)in tenths of a degree. - /// - IntFeedback TemperatureFeedback { get; } + IntFeedback TemperatureFeedback { get; } - /// - /// The temperature in Celsius format - /// - BoolFeedback TemperatureInCFeedback { get; } + /// + /// The temperature in Celsius format + /// + BoolFeedback TemperatureInCFeedback { get; } - /// - /// Sets the temperature format to Celsius or Fahrenheit - /// - /// If true, sets the format to Celsius; otherwise, sets it to Fahrenheit. - void SetTemperatureFormat(bool setToC); - } + /// + /// Sets the temperature format to Celsius or Fahrenheit + /// + /// If true, sets the format to Celsius; otherwise, sets it to Fahrenheit. + void SetTemperatureFormat(bool setToC); } + diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITransport.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITransport.cs index 917a4b19..4ff2d6ab 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITransport.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITransport.cs @@ -1,98 +1,97 @@ using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines the contract for ITransport +/// +public interface ITransport { - /// - /// Defines the contract for ITransport - /// - public interface ITransport + /// + /// Play button action + /// + /// determines if the button action is a press or release + void Play(bool pressRelease); + + /// + /// Pause button action + /// + /// determines if the button action is a press or release + void Pause(bool pressRelease); + + /// + /// Rewind button action + /// + /// determines if the button action is a press or release + void Rewind(bool pressRelease); + + /// + /// Fast Forward button action + /// + /// determines if the button action is a press or release + void FFwd(bool pressRelease); + + /// + /// Chapter Minus button action + /// + /// determines if the button action is a press or release + void ChapMinus(bool pressRelease); + + /// + /// Chapter Plus button action + /// + /// determines if the button action is a press or release + void ChapPlus(bool pressRelease); + + /// + /// Stop button action + /// + /// determines if the button action is a press or release + void Stop(bool pressRelease); + + /// + /// Record button action + /// + /// determines if the button action is a press or release + void Record(bool pressRelease); +} + +/// +/// ITransportExtensions class +/// +public static class ITransportExtensions +{ + /// + /// Attaches to trilist joins: Play:145, Pause:146, Stop:147, ChapPlus:148, ChapMinus:149, Rewind:150, Ffwd:151, Record:154 + /// + /// The ITransport device + /// The BasicTriList to link buttons to + public static void LinkButtons(this ITransport dev, BasicTriList triList) { - /// - /// Play button action - /// - /// determines if the button action is a press or release - void Play(bool pressRelease); - - /// - /// Pause button action - /// - /// determines if the button action is a press or release - void Pause(bool pressRelease); - - /// - /// Rewind button action - /// - /// determines if the button action is a press or release - void Rewind(bool pressRelease); - - /// - /// Fast Forward button action - /// - /// determines if the button action is a press or release - void FFwd(bool pressRelease); - - /// - /// Chapter Minus button action - /// - /// determines if the button action is a press or release - void ChapMinus(bool pressRelease); - - /// - /// Chapter Plus button action - /// - /// determines if the button action is a press or release - void ChapPlus(bool pressRelease); - - /// - /// Stop button action - /// - /// determines if the button action is a press or release - void Stop(bool pressRelease); - - /// - /// Record button action - /// - /// determines if the button action is a press or release - void Record(bool pressRelease); + triList.SetBoolSigAction(145, dev.Play); + triList.SetBoolSigAction(146, dev.Pause); + triList.SetBoolSigAction(147, dev.Stop); + triList.SetBoolSigAction(148, dev.ChapPlus); + triList.SetBoolSigAction(149, dev.ChapMinus); + triList.SetBoolSigAction(150, dev.Rewind); + triList.SetBoolSigAction(151, dev.FFwd); + triList.SetBoolSigAction(154, dev.Record); } /// - /// ITransportExtensions class + /// UnlinkButtons method /// - public static class ITransportExtensions + /// The ITransport device + /// The BasicTriList to unlink buttons from + public static void UnlinkButtons(this ITransport dev, BasicTriList triList) { - /// - /// Attaches to trilist joins: Play:145, Pause:146, Stop:147, ChapPlus:148, ChapMinus:149, Rewind:150, Ffwd:151, Record:154 - /// - /// The ITransport device - /// The BasicTriList to link buttons to - public static void LinkButtons(this ITransport dev, BasicTriList triList) - { - triList.SetBoolSigAction(145, dev.Play); - triList.SetBoolSigAction(146, dev.Pause); - triList.SetBoolSigAction(147, dev.Stop); - triList.SetBoolSigAction(148, dev.ChapPlus); - triList.SetBoolSigAction(149, dev.ChapMinus); - triList.SetBoolSigAction(150, dev.Rewind); - triList.SetBoolSigAction(151, dev.FFwd); - triList.SetBoolSigAction(154, dev.Record); - } - - /// - /// UnlinkButtons method - /// - /// The ITransport device - /// The BasicTriList to unlink buttons from - public static void UnlinkButtons(this ITransport dev, BasicTriList triList) - { - triList.ClearBoolSigAction(145); - triList.ClearBoolSigAction(146); - triList.ClearBoolSigAction(147); - triList.ClearBoolSigAction(148); - triList.ClearBoolSigAction(149); - triList.ClearBoolSigAction(150); - triList.ClearBoolSigAction(151); - triList.ClearBoolSigAction(154); - } + triList.ClearBoolSigAction(145); + triList.ClearBoolSigAction(146); + triList.ClearBoolSigAction(147); + triList.ClearBoolSigAction(148); + triList.ClearBoolSigAction(149); + triList.ClearBoolSigAction(150); + triList.ClearBoolSigAction(151); + triList.ClearBoolSigAction(154); } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITvPresetsProvider.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITvPresetsProvider.cs index 09b7066c..0d6fce71 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITvPresetsProvider.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/ITvPresetsProvider.cs @@ -1,15 +1,14 @@ using PepperDash.Essentials.Core.Presets; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Defines the contract for ITvPresetsProvider +/// +public interface ITvPresetsProvider { /// - /// Defines the contract for ITvPresetsProvider + /// The TV presets model /// - public interface ITvPresetsProvider - { - /// - /// The TV presets model - /// - DevicePresetsModel TvPresets { get; } - } -} \ No newline at end of file + DevicePresetsModel TvPresets { get; } +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IUiDisplayInfo.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IUiDisplayInfo.cs index 231abf1f..274cb06a 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IUiDisplayInfo.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IUiDisplayInfo.cs @@ -1,7 +1,7 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Defines the contract for IUiDisplayInfo /// @@ -11,5 +11,4 @@ namespace PepperDash.Essentials.Core /// Display UI Type /// uint DisplayUiType { get; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IWarmingCooling.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IWarmingCooling.cs index e69bf35c..54ac37f8 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IWarmingCooling.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IWarmingCooling.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Defines a class that has warm up and cool down /// @@ -20,5 +20,4 @@ namespace PepperDash.Essentials.Core /// Feedback indicating whether the device is cooling down /// BoolFeedback IsCoolingDownFeedback { get; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/LanguageLabel.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/LanguageLabel.cs index c8a45592..7f83a829 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/LanguageLabel.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/LanguageLabel.cs @@ -1,28 +1,26 @@ -using System; -using PepperDash.Core; + +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces + +/// +/// Represents a LanguageLabel +/// +public class LanguageLabel { /// - /// Represents a LanguageLabel + /// Gets or sets the Key /// - public class LanguageLabel - { - /// - /// Gets or sets the Key - /// - public string Key { get; set; } - /// - /// Gets or sets the Description - /// - public string Description { get; set; } - /// - /// Gets or sets the DisplayText - /// - public string DisplayText { get; set; } - /// - /// Gets or sets the JoinNumber - /// - public uint JoinNumber { get; set; } - } -} \ No newline at end of file + public string Key { get; set; } + /// + /// Gets or sets the Description + /// + public string Description { get; set; } + /// + /// Gets or sets the DisplayText + /// + public string DisplayText { get; set; } + /// + /// Gets or sets the JoinNumber + /// + public uint JoinNumber { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/Template.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/Template.cs index 31f71df9..bf8dedd6 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/Template.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/Template.cs @@ -3,7 +3,6 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.SmartObjects; -namespace PepperDash.Essentials.Core -{ - -} \ No newline at end of file +namespace PepperDash.Essentials.Core; + + \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/AudioControlListItemBase.cs b/src/PepperDash.Essentials.Core/Devices/AudioControlListItemBase.cs index 038f80a7..ec555c79 100644 --- a/src/PepperDash.Essentials.Core/Devices/AudioControlListItemBase.cs +++ b/src/PepperDash.Essentials.Core/Devices/AudioControlListItemBase.cs @@ -5,41 +5,40 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Base class for items that can be added to an AudioControlList. Contains properties that are common to all items, such as the parent device key and an optional item key. Also includes properties for display purposes, such as a name and order. +/// +public abstract class AudioControlListItemBase { /// - /// Base class for audio control list items + /// Key of the parent device in the DeviceManager /// - public abstract class AudioControlListItemBase - { - /// - /// Key of the parent device in the DeviceManager - /// - [JsonProperty("parentDeviceKey")] - public string ParentDeviceKey { get; set; } + [JsonProperty("parentDeviceKey")] + public string ParentDeviceKey { get; set; } - /// - /// Optional key of the item in the parent device - /// - [JsonProperty("itemKey")] - public string ItemKey { get; set; } + /// + /// Optional key of the item in the parent device + /// + [JsonProperty("itemKey")] + public string ItemKey { get; set; } - /// - /// A name that will override the items's name on the UI - /// - [JsonProperty("name")] - public string Name { get; set; } + /// + /// A name that will override the items's name on the UI + /// + [JsonProperty("name")] + public string Name { get; set; } - /// - /// Indicates if the item should be included in the user accessible list - /// - [JsonProperty("includeInUserList")] - public bool IncludeInUserList { get; set; } + /// + /// Indicates if the item should be included in the user accessible list + /// + [JsonProperty("includeInUserList")] + public bool IncludeInUserList { get; set; } - /// - /// Used to specify the order of the items in the source list when displayed - /// - [JsonProperty("order")] - public int Order { get; set; } - } + /// + /// Used to specify the order of the items in the source list when displayed + /// + [JsonProperty("order")] + public int Order { get; set; } } diff --git a/src/PepperDash.Essentials.Core/Devices/AudioInterfaces.cs b/src/PepperDash.Essentials.Core/Devices/AudioInterfaces.cs index cc886636..a9e0dc92 100644 --- a/src/PepperDash.Essentials.Core/Devices/AudioInterfaces.cs +++ b/src/PepperDash.Essentials.Core/Devices/AudioInterfaces.cs @@ -6,8 +6,8 @@ using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Enumeration of AudioChangeType values @@ -50,5 +50,4 @@ namespace PepperDash.Essentials.Core ChangeType = changeType; AudioDevice = device; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/CameraListItem.cs b/src/PepperDash.Essentials.Core/Devices/CameraListItem.cs index a154c083..91ca3c83 100644 --- a/src/PepperDash.Essentials.Core/Devices/CameraListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/CameraListItem.cs @@ -1,82 +1,82 @@ using Newtonsoft.Json; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents an item in a camera list, which can be used to display camera sources in a user interface. Contains properties for the device key, preferred name, icon, and order of the item in the list. Also includes a property to get the associated camera device from the DeviceManager based on the device key. +/// +public class CameraListItem { + /// - /// Represents a CameraListItem + /// Key of the camera device in the DeviceManager /// - public class CameraListItem + [JsonProperty("deviceKey")] + public string DeviceKey { get; set; } + + /// + /// Returns the source Device for this, if it exists in DeviceManager + /// + [JsonIgnore] + public Device CameraDevice { - /// - /// Key of the camera device - /// - [JsonProperty("deviceKey")] - public string DeviceKey { get; set; } - - /// - /// Returns the source Device for this, if it exists in DeviceManager - /// - [JsonIgnore] - public Device CameraDevice + get { - get - { - if (_cameraDevice == null) - _cameraDevice = DeviceManager.GetDeviceForKey(DeviceKey) as Device; - return _cameraDevice; - } + if (_cameraDevice == null) + _cameraDevice = DeviceManager.GetDeviceForKey(DeviceKey) as Device; + return _cameraDevice; } - Device _cameraDevice; - - /// - /// Gets either the source's Name or this AlternateName property, if - /// defined. If source doesn't exist, returns "Missing source" - /// - [JsonProperty("preferredName")] - public string PreferredName - { - get - { - if (string.IsNullOrEmpty(Name)) - { - if (CameraDevice == null) - return "---"; - return CameraDevice.Name; - } - return Name; - } - } - - /// - /// A name that will override the source's name on the UI - /// - [JsonProperty("name")] - public string Name { get; set; } - - - /// - /// Specifies and icon for the source list item - /// - [JsonProperty("icon")] - public string Icon { get; set; } - - /// - /// Alternate icon - /// - [JsonProperty("altIcon", NullValueHandling = NullValueHandling.Ignore)] - public string AltIcon { get; set; } - - /// - /// Indicates if the item should be included in the user facing list - /// - [JsonProperty("includeInUserList")] - public bool IncludeInUserList { get; set; } - - /// - /// Used to specify the order of the items in the source list when displayed - /// - [JsonProperty("order")] - public int Order { get; set; } } + Device _cameraDevice; + + /// + /// Gets either the source's Name or this AlternateName property, if + /// defined. If source doesn't exist, returns "Missing source" + /// + [JsonProperty("preferredName")] + public string PreferredName + { + get + { + if (string.IsNullOrEmpty(Name)) + { + if (CameraDevice == null) + return "---"; + return CameraDevice.Name; + } + return Name; + } + } + + /// + /// A name that will override the source's name on the UI + /// + [JsonProperty("name")] + public string Name { get; set; } + + + /// + /// Specifies and icon for the source list item + /// + [JsonProperty("icon")] + public string Icon { get; set; } + + /// + /// Alternate icon + /// + [JsonProperty("altIcon", NullValueHandling = NullValueHandling.Ignore)] + public string AltIcon { get; set; } + + /// + /// Indicates if the item should be included in the user facing list + /// + [JsonProperty("includeInUserList")] + public bool IncludeInUserList { get; set; } + + /// + /// Used to specify the order of the items in the source list when displayed + /// + [JsonProperty("order")] + public int Order { get; set; } } diff --git a/src/PepperDash.Essentials.Core/Devices/CodecInterfaces.cs b/src/PepperDash.Essentials.Core/Devices/CodecInterfaces.cs index b9d8a28f..d757cd5a 100644 --- a/src/PepperDash.Essentials.Core/Devices/CodecInterfaces.cs +++ b/src/PepperDash.Essentials.Core/Devices/CodecInterfaces.cs @@ -4,107 +4,106 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Adds control of codec receive volume +/// +public interface IReceiveVolume +{ + // Break this out into 3 interfaces + + /// + /// Sets the receive volume level + /// + /// volume level to set + void SetReceiveVolume(ushort level); + + /// + /// Mutes the receive audio + /// + void ReceiveMuteOn(); + + /// + /// Unmutes the receive audio + /// + void ReceiveMuteOff(); + + /// + /// Toggles the receive mute state + /// + void ReceiveMuteToggle(); + + /// + /// Feedback for the receive volume level + /// + IntFeedback ReceiveLevelFeedback { get; } + + /// + /// Feedback for the receive mute state + /// + BoolFeedback ReceiveMuteIsOnFeedback { get; } +} + +/// +/// Defines the contract for ITransmitVolume +/// +public interface ITransmitVolume { /// - /// Adds control of codec receive volume + /// Sets the transmit volume level /// - public interface IReceiveVolume - { - // Break this out into 3 interfaces - - /// - /// Sets the receive volume level - /// - /// volume level to set - void SetReceiveVolume(ushort level); - - /// - /// Mutes the receive audio - /// - void ReceiveMuteOn(); - - /// - /// Unmutes the receive audio - /// - void ReceiveMuteOff(); - - /// - /// Toggles the receive mute state - /// - void ReceiveMuteToggle(); - - /// - /// Feedback for the receive volume level - /// - IntFeedback ReceiveLevelFeedback { get; } - - /// - /// Feedback for the receive mute state - /// - BoolFeedback ReceiveMuteIsOnFeedback { get; } - } + /// volume level to set + void SetTransmitVolume(ushort level); /// - /// Defines the contract for ITransmitVolume + /// Mutes the transmit audio /// - public interface ITransmitVolume - { - /// - /// Sets the transmit volume level - /// - /// volume level to set - void SetTransmitVolume(ushort level); - - /// - /// Mutes the transmit audio - /// - void TransmitMuteOn(); - - /// - /// Unmutes the transmit audio - /// - void TransmitMuteOff(); - - /// - /// Toggles the transmit mute state - /// - void TransmitMuteToggle(); - - /// - /// Feedback for the transmit volume level - /// - IntFeedback TransmitLevelFeedback { get; } - - /// - /// Feedback for the transmit mute state - /// - BoolFeedback TransmitMuteIsOnFeedback { get; } - } + void TransmitMuteOn(); /// - /// Defines the contract for IPrivacy + /// Unmutes the transmit audio /// - public interface IPrivacy - { - /// - /// Enables privacy mode - /// - void PrivacyModeOn(); + void TransmitMuteOff(); - /// - /// Disables privacy mode - /// - void PrivacyModeOff(); + /// + /// Toggles the transmit mute state + /// + void TransmitMuteToggle(); - /// - /// Toggles privacy mode - /// - void PrivacyModeToggle(); + /// + /// Feedback for the transmit volume level + /// + IntFeedback TransmitLevelFeedback { get; } - /// - /// Feedback for the privacy mode state - /// - BoolFeedback PrivacyModeIsOnFeedback { get; } - } -} \ No newline at end of file + /// + /// Feedback for the transmit mute state + /// + BoolFeedback TransmitMuteIsOnFeedback { get; } +} + +/// +/// Defines the contract for IPrivacy +/// +public interface IPrivacy +{ + /// + /// Enables privacy mode + /// + void PrivacyModeOn(); + + /// + /// Disables privacy mode + /// + void PrivacyModeOff(); + + /// + /// Toggles privacy mode + /// + void PrivacyModeToggle(); + + /// + /// Feedback for the privacy mode state + /// + BoolFeedback PrivacyModeIsOnFeedback { get; } +} diff --git a/src/PepperDash.Essentials.Core/Devices/CrestronProcessor.cs b/src/PepperDash.Essentials.Core/Devices/CrestronProcessor.cs index 8c31d4c6..29481b34 100644 --- a/src/PepperDash.Essentials.Core/Devices/CrestronProcessor.cs +++ b/src/PepperDash.Essentials.Core/Devices/CrestronProcessor.cs @@ -8,56 +8,45 @@ using PepperDash.Core; using PepperDash.Essentials.Core.CrestronIO; using Serilog.Events; -namespace PepperDash.Essentials.Core.Devices +namespace PepperDash.Essentials.Core.Devices; + +/// +/// This wrapper class is meant to allow interfaces to be applied to any Crestron processor +/// +public class CrestronProcessor : Device, ISwitchedOutputCollection { - /// - /// This wrapper class is meant to allow interfaces to be applied to any Crestron processor - /// - public class CrestronProcessor : Device, ISwitchedOutputCollection + public Dictionary SwitchedOutputs { get; private set; } + + public Crestron.SimplSharpPro.CrestronControlSystem Processor { get; private set; } + + public CrestronProcessor(string key) + : base(key) { - /// - /// Collection of switched outputs (relays) on the processor - /// - public Dictionary SwitchedOutputs { get; private set; } + SwitchedOutputs = new Dictionary(); + Processor = Global.ControlSystem; - /// - /// The underlying CrestronControlSystem processor - /// - public Crestron.SimplSharpPro.CrestronControlSystem Processor { get; private set; } + GetRelays(); + } - /// - /// Constructor - /// - /// key for the processor - public CrestronProcessor(string key) - : base(key) + /// + /// Creates a GenericRelayDevice for each relay on the processor and adds them to the SwitchedOutputs collection + /// + void GetRelays() + { + try { - SwitchedOutputs = new Dictionary(); - Processor = Global.ControlSystem; - - GetRelays(); - } - - /// - /// Creates a GenericRelayDevice for each relay on the processor and adds them to the SwitchedOutputs collection - /// - void GetRelays() - { - try + if (Processor.SupportsRelay) { - if (Processor.SupportsRelay) + for (uint i = 1; i <= Processor.NumberOfRelayPorts; i++) { - for (uint i = 1; i <= Processor.NumberOfRelayPorts; i++) - { - var relay = new GenericRelayDevice(string.Format("{0}-relay-{1}", this.Key, i), Processor.RelayPorts[i]); - SwitchedOutputs.Add(i, relay); - } + var relay = new GenericRelayDevice(string.Format("{0}-relay-{1}", this.Key, i), Processor.RelayPorts[i]); + SwitchedOutputs.Add(i, relay); } } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Error Getting Relays from processor:\n '{0}'", e); - } + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Error Getting Relays from processor:\n '{0}'", e); } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs b/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs index 5a66c0b3..03f7af0e 100644 --- a/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs @@ -3,122 +3,123 @@ using Newtonsoft.Json; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a destination item in a routing system that can receive audio/video signals. +/// Contains information about the destination device, its properties, and location settings. +/// +public class DestinationListItem { + /// - /// Represents a destination item in a routing system that can receive audio/video signals. - /// Contains information about the destination device, its properties, and location settings. + /// Gets or sets the key identifier for the sink device that this destination represents. /// - public class DestinationListItem + [JsonProperty("sinkKey")] + public string SinkKey { get; set; } + + private EssentialsDevice _sinkDevice; + + /// + /// Gets the actual device instance for this destination. + /// Lazily loads the device from the DeviceManager using the SinkKey. + /// + [JsonIgnore] + public EssentialsDevice SinkDevice { - /// - /// Gets or sets the key identifier for the sink device that this destination represents. - /// - [JsonProperty("sinkKey")] - public string SinkKey { get; set; } - - private EssentialsDevice _sinkDevice; - - /// - /// Gets the actual device instance for this destination. - /// Lazily loads the device from the DeviceManager using the SinkKey. - /// - [JsonIgnore] - public EssentialsDevice SinkDevice - { - get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as EssentialsDevice); } - } - - /// - /// Gets the preferred display name for this destination. - /// Returns the custom Name if set, otherwise returns the SinkDevice name, or "---" if no device is found. - /// - [JsonProperty("preferredName")] - public string PreferredName - { - get - { - if (!string.IsNullOrEmpty(Name)) - { - return Name; - } - - return SinkDevice == null ? "---" : SinkDevice.Name; - } - } - - /// - /// Gets or sets the custom name for this destination. - /// If set, this name will be used as the PreferredName instead of the device name. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// Gets or sets a value indicating whether this destination should be included in destination lists. - /// - [JsonProperty("includeInDestinationList")] - public bool IncludeInDestinationList { get; set; } - - /// - /// Gets or sets the display order for this destination in lists. - /// Lower values appear first in sorted lists. - /// - [JsonProperty("order")] - public int Order { get; set; } - - /// - /// Gets or sets the surface location identifier for this destination. - /// Used to specify which surface or screen this destination is located on. - /// - [JsonProperty("surfaceLocation")] - public int SurfaceLocation { get; set; } - - /// - /// Gets or sets the vertical location position for this destination. - /// Used for spatial positioning in multi-display configurations. - /// - [JsonProperty("verticalLocation")] - public int VerticalLocation { get; set; } - - /// - /// Gets or sets the horizontal location position for this destination. - /// Used for spatial positioning in multi-display configurations. - /// - [JsonProperty("horizontalLocation")] - public int HorizontalLocation { get; set; } - - /// - /// Gets or sets the signal type that this destination can receive (Audio, Video, AudioVideo, etc.). - /// - [JsonProperty("sinkType")] - public eRoutingSignalType SinkType { get; set; } - - /// - /// Gets or sets a value indicating whether this destination is used for codec content sharing. - /// - [JsonProperty("isCodecContentDestination")] - public bool isCodecContentDestination { get; set; } - - /// - /// Gets or sets a value indicating whether this destination is used for program audio output. - /// - [JsonProperty("isProgramAudioDestination")] - public bool isProgramAudioDestination { get; set; } - - /// - /// Gets or sets a value indicating whether this destination supports USB connections. - /// Indicates if the destination can handle USB functionality, such as USB signal routing or device connections. - /// This property is used to determine compatibility with USB-based devices or systems. - /// - [JsonProperty("supportsUsb")] - public bool SupportsUsb { get; set; } - - /// - /// The key of the destination port associated with this destination item - /// This is used to identify the specific port on the destination device that this item refers to for advanced routing - /// - [JsonProperty("destinationPortKey")] - public string DestinationPortKey { get; set; } + get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as EssentialsDevice); } } + + /// + /// Gets the preferred display name for this destination. + /// Returns the custom Name if set, otherwise returns the SinkDevice name, or "---" if no device is found. + /// + [JsonProperty("preferredName")] + public string PreferredName + { + get + { + if (!string.IsNullOrEmpty(Name)) + { + return Name; + } + + return SinkDevice == null ? "---" : SinkDevice.Name; + } + } + + /// + /// Gets or sets the custom name for this destination. + /// If set, this name will be used as the PreferredName instead of the device name. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether this destination should be included in destination lists. + /// + [JsonProperty("includeInDestinationList")] + public bool IncludeInDestinationList { get; set; } + + /// + /// Gets or sets the display order for this destination in lists. + /// Lower values appear first in sorted lists. + /// + [JsonProperty("order")] + public int Order { get; set; } + + /// + /// Gets or sets the surface location identifier for this destination. + /// Used to specify which surface or screen this destination is located on. + /// + [JsonProperty("surfaceLocation")] + public int SurfaceLocation { get; set; } + + /// + /// Gets or sets the vertical location position for this destination. + /// Used for spatial positioning in multi-display configurations. + /// + [JsonProperty("verticalLocation")] + public int VerticalLocation { get; set; } + + /// + /// Gets or sets the horizontal location position for this destination. + /// Used for spatial positioning in multi-display configurations. + /// + [JsonProperty("horizontalLocation")] + public int HorizontalLocation { get; set; } + + /// + /// Gets or sets the signal type that this destination can receive (Audio, Video, AudioVideo, etc.). + /// + [JsonProperty("sinkType")] + public eRoutingSignalType SinkType { get; set; } + + /// + /// Gets or sets a value indicating whether this destination is used for codec content sharing. + /// + [JsonProperty("isCodecContentDestination")] + public bool isCodecContentDestination { get; set; } + + /// + /// Gets or sets a value indicating whether this destination is used for program audio output. + /// + [JsonProperty("isProgramAudioDestination")] + public bool isProgramAudioDestination { get; set; } + + /// + /// Gets or sets a value indicating whether this destination supports USB connections. + /// Indicates if the destination can handle USB functionality, such as USB signal routing or device connections. + /// This property is used to determine compatibility with USB-based devices or systems. + /// + [JsonProperty("supportsUsb")] + public bool SupportsUsb { get; set; } + + /// + /// The key of the destination port associated with this destination item + /// This is used to identify the specific port on the destination device that this item refers to for advanced routing + /// + [JsonProperty("destinationPortKey")] + public string DestinationPortKey { get; set; } + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceApiBase.cs b/src/PepperDash.Essentials.Core/Devices/DeviceApiBase.cs index 28ac13dc..9d15b256 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceApiBase.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceApiBase.cs @@ -4,21 +4,20 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core.Devices +namespace PepperDash.Essentials.Core.Devices; + +/// +/// Base class for all Device APIs +/// +public abstract class DeviceApiBase { /// - /// Base class for all Device APIs + /// Action API dictionary /// - public abstract class DeviceApiBase - { - /// - /// Action API dictionary - /// - public Dictionary ActionApi { get; protected set; } + public Dictionary ActionApi { get; protected set; } - /// - /// Feedback API dictionary - /// - public Dictionary FeedbackApi { get; protected set; } - } -} \ No newline at end of file + /// + /// Feedback API dictionary + /// + public Dictionary FeedbackApi { get; protected set; } +} diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceFeedbackExtensions.cs b/src/PepperDash.Essentials.Core/Devices/DeviceFeedbackExtensions.cs index 0e7595e1..e8bb635a 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceFeedbackExtensions.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceFeedbackExtensions.cs @@ -6,30 +6,29 @@ using Crestron.SimplSharp; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Extension methods for working with device feedback properties. +/// +public static class DeviceFeedbackExtensions { /// - /// DeviceFeedbackExtensions class + /// Attempts to get and return a feedback property from a device by name. + /// If unsuccessful, returns null. /// - public static class DeviceFeedbackExtensions + /// + /// + /// + public static Feedback GetFeedbackProperty(this Device device, string propertyName) { - /// - /// Attempts to get and return a feedback property from a device by name. - /// If unsuccessful, returns null. - /// - /// device to get feedback from - /// name of the feedback property - /// Feedback property if found, otherwise null - public static Feedback GetFeedbackProperty(this Device device, string propertyName) + var feedback = DeviceJsonApi.GetPropertyByName(device.Key, propertyName) as Feedback; + + if (feedback != null) { - var feedback = DeviceJsonApi.GetPropertyByName(device.Key, propertyName) as Feedback; - - if (feedback != null) - { - return feedback; - } - - return null; + return feedback; } + + return null; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs index c4ec2ca9..6ba640d4 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs @@ -9,528 +9,530 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Provides methods for interacting with devices using JSON-formatted commands. +/// +public class DeviceJsonApi { /// - /// Represents a DeviceJsonApi + /// Executes a method on a device based on a JSON-formatted command string. /// - public class DeviceJsonApi + /// + public static void DoDeviceActionWithJson(string json) { - /// - /// DoDeviceActionWithJson method - /// - /// json method - public static void DoDeviceActionWithJson(string json) + if (String.IsNullOrEmpty(json)) { - if (String.IsNullOrEmpty(json)) + CrestronConsole.ConsoleCommandResponse( + "Please provide a JSON object matching the format {\"deviceKey\":\"myDevice\", \"methodName\":\"someMethod\", \"params\": [\"param1\", true]}.\r\nIf the method has no parameters, the \"params\" object may be omitted."); + return; + } + try + { + var action = JsonConvert.DeserializeObject(json); + + DoDeviceAction(action); + } + catch (Exception) + { + CrestronConsole.ConsoleCommandResponse("Incorrect format for JSON. Please check that the format matches {\"deviceKey\":\"myDevice\", \"methodName\":\"someMethod\", \"params\": [\"param1\", true]}"); + } + + } + + + /// + /// Executes a method on a device based on a JSON-formatted command string, awaiting the result if the method is asynchronous. + /// + /// + public static void DoDeviceAction(DeviceActionWrapper action) + { + var key = action.DeviceKey; + var obj = FindObjectOnPath(key); + if (obj == null) + { + CrestronConsole.ConsoleCommandResponse("Unable to find object at path {0}", key); + return; + } + + if (action.Params == null) + { + //no params, so setting action.Params to empty array + action.Params = new object[0]; + } + + Type t = obj.GetType(); + try + { + var methods = t.GetMethods().Where(m => m.Name == action.MethodName).ToList(); + + var method = methods.Count == 1 ? methods[0] : methods.FirstOrDefault(m => m.GetParameters().Length == action.Params.Length); + + if (method == null) { CrestronConsole.ConsoleCommandResponse( - "Please provide a JSON object matching the format {\"deviceKey\":\"myDevice\", \"methodName\":\"someMethod\", \"params\": [\"param1\", true]}.\r\nIf the method has no parameters, the \"params\" object may be omitted."); + "Unable to find method with name {0} and that matches parameters {1}", action.MethodName, + action.Params); return; } - try - { - var action = JsonConvert.DeserializeObject(json); + var mParams = method.GetParameters(); - DoDeviceAction(action); - } - catch (Exception) - { - CrestronConsole.ConsoleCommandResponse("Incorrect format for JSON. Please check that the format matches {\"deviceKey\":\"myDevice\", \"methodName\":\"someMethod\", \"params\": [\"param1\", true]}"); - } + var convertedParams = mParams + .Select((p, i) => ConvertType(action.Params[i], p.ParameterType)) + .ToArray(); - } - - - /// - /// DoDeviceAction method - /// - /// action method - public static void DoDeviceAction(DeviceActionWrapper action) - { - var key = action.DeviceKey; - var obj = FindObjectOnPath(key); - if (obj == null) - { - CrestronConsole.ConsoleCommandResponse("Unable to find object at path {0}", key); - return; - } - - if (action.Params == null) - { - //no params, so setting action.Params to empty array - action.Params = new object[0]; - } - - Type t = obj.GetType(); - try - { - var methods = t.GetMethods().Where(m => m.Name == action.MethodName).ToList(); - - var method = methods.Count == 1 ? methods[0] : methods.FirstOrDefault(m => m.GetParameters().Length == action.Params.Length); - - if (method == null) - { - CrestronConsole.ConsoleCommandResponse( - "Unable to find method with name {0} and that matches parameters {1}", action.MethodName, - action.Params); - return; - } - var mParams = method.GetParameters(); - - var convertedParams = mParams - .Select((p, i) => ConvertType(action.Params[i], p.ParameterType)) - .ToArray(); - - Task.Run(() => - { - try - { - Debug.LogMessage(LogEventLevel.Verbose, "Calling method {methodName} on device {deviceKey}", null, method.Name, action.DeviceKey); - method.Invoke(obj, convertedParams); - } - catch (Exception e) - { - Debug.LogMessage(e, "Error invoking method {methodName} on device {deviceKey}", null, method.Name, action.DeviceKey); - } - }); - - CrestronConsole.ConsoleCommandResponse("Method {0} successfully called on device {1}", method.Name, - action.DeviceKey); - } - catch (Exception ex) - { - CrestronConsole.ConsoleCommandResponse("Unable to call method with name {0}. {1}", action.MethodName, - ex.Message); - } - } - - /// - /// DoDeviceActionAsync method - /// - /// action method - public static async Task DoDeviceActionAsync(DeviceActionWrapper action) - { - var key = action.DeviceKey; - var obj = FindObjectOnPath(key); - if (obj == null) - { - Debug.LogMessage(LogEventLevel.Warning, "Unable to find object at path {deviceKey}", null, key); - return; - } - - if (action.Params == null) - { - //no params, so setting action.Params to empty array - action.Params = new object[0]; - } - - Type t = obj.GetType(); - try - { - var methods = t.GetMethods().Where(m => m.Name == action.MethodName).ToList(); - - var method = methods.Count == 1 ? methods[0] : methods.FirstOrDefault(m => m.GetParameters().Length == action.Params.Length); - - if (method == null) - { - Debug.LogMessage(LogEventLevel.Warning, - "Unable to find method with name {methodName} and that matches parameters {@parameters}", null, action.MethodName, - action.Params); - return; - } - var mParams = method.GetParameters(); - - var convertedParams = mParams - .Select((p, i) => ConvertType(action.Params[i], p.ParameterType)) - .ToArray(); - - try - { - Debug.LogMessage(LogEventLevel.Verbose, "Calling method {methodName} on device {deviceKey} with {@params}", null, method.Name, action.DeviceKey, action.Params); - var result = method.Invoke(obj, convertedParams); - - // If the method returns a Task, await it - if (result is Task task) - { - await task; - } - // If the method returns a Task, await it - else if (result != null && result.GetType().IsGenericType && result.GetType().GetGenericTypeDefinition() == typeof(Task<>)) - { - await (Task)result; - } - } - catch (Exception e) - { - Debug.LogMessage(e, "Error invoking method {methodName} on device {deviceKey}", null, method.Name, action.DeviceKey); - } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Unable to call method with name {methodName} with {@parameters}", null, action.MethodName, action.Params); - } - } - - private static object ConvertType(object value, Type conversionType) - { - if (!conversionType.IsEnum) - { - return Convert.ChangeType(value, conversionType, System.Globalization.CultureInfo.InvariantCulture); - } - - var stringValue = Convert.ToString(value); - - if (String.IsNullOrEmpty(stringValue)) - { - throw new InvalidCastException( - String.Format("{0} cannot be converted to a string prior to conversion to enum")); - } - return Enum.Parse(conversionType, stringValue, true); - } - - /// - /// Gets the properties on a device - /// - /// The path to the device object - /// A JSON string representing the properties of the device - public static string GetProperties(string deviceObjectPath) - { - var obj = FindObjectOnPath(deviceObjectPath); - if (obj == null) - return "{ \"error\":\"No Device\"}"; - - Type t = obj.GetType(); - // get the properties and set them into a new collection of NameType wrappers - var props = t.GetProperties().Select(p => new PropertyNameType(p, obj)); - return JsonConvert.SerializeObject(props, Formatting.Indented); - } - - /// - /// Gets a property from a device path by name - /// - /// The path to the device object - /// The name of the property to get - /// The value of the property - public static object GetPropertyByName(string deviceObjectPath, string propertyName) - { - var dev = FindObjectOnPath(deviceObjectPath); - if (dev == null) - return "{ \"error\":\"No Device\"}"; - - object prop = dev.GetType().GetProperty(propertyName).GetValue(dev, null); - - // var prop = t.GetProperty(propertyName); - if (prop != null) - { - return prop; - } - else - { - Debug.LogMessage(LogEventLevel.Debug, "Unable to find Property: {0} on Device with path: {1}", propertyName, deviceObjectPath); - return null; - } - } - - /// - /// Gets the methods on a device - /// - /// The path to the device object - /// A JSON string representing the methods of the device - public static string GetMethods(string deviceObjectPath) - { - var obj = FindObjectOnPath(deviceObjectPath); - if (obj == null) - return "{ \"error\":\"No Device\"}"; - - // Package up method names using helper objects - Type t = obj.GetType(); - var methods = t.GetMethods() - .Where(m => !m.IsSpecialName) - .Select(p => new MethodNameParams(p)); - return JsonConvert.SerializeObject(methods, Formatting.Indented); - } - - /// - /// Gets the API methods on a device - /// - /// The path to the device object - /// A JSON string representing the API methods of the device - public static string GetApiMethods(string deviceObjectPath) - { - var obj = FindObjectOnPath(deviceObjectPath); - if (obj == null) - return "{ \"error\":\"No Device\"}"; - - // Package up method names using helper objects - Type t = obj.GetType(); - var methods = t.GetMethods() - .Where(m => !m.IsSpecialName) - .Where(m => m.GetCustomAttributes(typeof(ApiAttribute), true).Any()) - .Select(p => new MethodNameParams(p)); - return JsonConvert.SerializeObject(methods, Formatting.Indented); - } - - - /// - /// FindObjectOnPath method - /// - /// The path to the device object - /// The object found at the specified path - public static object FindObjectOnPath(string deviceObjectPath) - { - var path = deviceObjectPath.Split('.'); - - var dev = DeviceManager.GetDeviceForKey(path[0]); - if (dev == null) - { - Debug.LogMessage(LogEventLevel.Information, "Device {0} not found", path[0]); - return null; - } - - // loop through any dotted properties - object obj = dev; - if (path.Length > 1) - { - for (int i = 1; i < path.Length; i++) - { - var objName = path[i]; - string indexStr = null; - var indexOpen = objName.IndexOf('['); - if (indexOpen != -1) - { - var indexClose = objName.IndexOf(']'); - if (indexClose == -1) - { - Debug.LogMessage(LogEventLevel.Information, dev, "ERROR Unmatched index brackets"); - return null; - } - // Get the index and strip quotes if any - indexStr = objName.Substring(indexOpen + 1, indexClose - indexOpen - 1).Replace("\"", ""); - objName = objName.Substring(0, indexOpen); - Debug.LogMessage(LogEventLevel.Information, dev, " Checking for collection '{0}', index '{1}'", objName, indexStr); - } - - Type oType = obj.GetType(); - var prop = oType.GetProperty(objName); - if (prop == null) - { - Debug.LogMessage(LogEventLevel.Information, dev, "Property {0} not found on {1}", objName, path[i - 1]); - return null; - } - // if there's an index, try to get the property - if (indexStr != null) - { - if (!typeof(ICollection).IsAssignableFrom(prop.PropertyType)) - { - Debug.LogMessage(LogEventLevel.Information, dev, "Property {0} is not collection", objName); - return null; - } - var collection = prop.GetValue(obj, null) as ICollection; - // Get the indexed items "property" - var indexedPropInfo = prop.PropertyType.GetProperty("Item"); - // These are the parameters for the indexing. Only care about one - var indexParams = indexedPropInfo.GetIndexParameters(); - if (indexParams.Length > 0) - { - Debug.LogMessage(LogEventLevel.Information, " Indexed, param type: {0}", indexParams[0].ParameterType.Name); - var properParam = Convert.ChangeType(indexStr, indexParams[0].ParameterType, - System.Globalization.CultureInfo.InvariantCulture); - try - { - obj = indexedPropInfo.GetValue(collection, new object[] { properParam }); - } - // if the index is bad, catch it here. - catch (TargetInvocationException e) - { - if (e.InnerException is ArgumentOutOfRangeException) - Debug.LogMessage(LogEventLevel.Information, " Index Out of range"); - else if (e.InnerException is KeyNotFoundException) - Debug.LogMessage(LogEventLevel.Information, " Key not found"); - return null; - } - } - - } - else - obj = prop.GetValue(obj, null); - } - } - return obj; - } - - /// - /// Sets a property on an object. - /// - /// The path to the device object - /// A JSON string representing the result of setting the property - public static string SetProperty(string deviceObjectPath) - { - throw new NotImplementedException("This could be really useful. Finish it please"); - - //var obj = FindObjectOnPath(deviceObjectPath); - //if (obj == null) - // return "{\"error\":\"No object found\"}"; - - //Type t = obj.GetType(); - - - //// get the properties and set them into a new collection of NameType wrappers - //var props = t.GetProperties().Select(p => new PropertyNameType(p, obj)); - //return JsonConvert.SerializeObject(props, Formatting.Indented); - } - - - } - - /// - /// Represents a DeviceActionWrapper - /// - public class DeviceActionWrapper - { - /// - /// Gets or sets the DeviceKey - /// - public string DeviceKey { get; set; } - - /// - /// Gets or sets the MethodName - /// - public string MethodName { get; set; } - - /// - /// Gets or sets the Params - /// - public object[] Params { get; set; } - } - - /// - /// Represents a PropertyNameType - /// - public class PropertyNameType - { - private object Parent; - - /// - /// Gets or sets the PropInfo - /// - [JsonIgnore] - public PropertyInfo PropInfo { get; private set; } - - /// - /// Gets or sets the Name - /// - public string Name { get { return PropInfo.Name; } } - - /// - /// Gets or sets the Type - /// - public string Type { get { return PropInfo.PropertyType.Name; } } - - /// - /// Gets or sets the Value - /// - public string Value - { - get - { - if (PropInfo.CanRead) + Task.Run(() => { try { - return PropInfo.GetValue(Parent, null).ToString(); + Debug.LogMessage(LogEventLevel.Verbose, "Calling method {methodName} on device {deviceKey}", null, method.Name, action.DeviceKey); + method.Invoke(obj, convertedParams); } - catch (Exception) + catch (Exception e) { + Debug.LogMessage(e, "Error invoking method {methodName} on device {deviceKey}", null, method.Name, action.DeviceKey); + } + }); + + CrestronConsole.ConsoleCommandResponse("Method {0} successfully called on device {1}", method.Name, + action.DeviceKey); + } + catch (Exception ex) + { + CrestronConsole.ConsoleCommandResponse("Unable to call method with name {0}. {1}", action.MethodName, + ex.Message); + } + } + + /// + /// Executes a method on a device based on a JSON-formatted command string, awaiting the result if the method is asynchronous. + /// + /// + /// + public static async Task DoDeviceActionAsync(DeviceActionWrapper action) + { + var key = action.DeviceKey; + var obj = FindObjectOnPath(key); + if (obj == null) + { + Debug.LogMessage(LogEventLevel.Warning, "Unable to find object at path {deviceKey}", null, key); + return; + } + + if (action.Params == null) + { + //no params, so setting action.Params to empty array + action.Params = new object[0]; + } + + Type t = obj.GetType(); + try + { + var methods = t.GetMethods().Where(m => m.Name == action.MethodName).ToList(); + + var method = methods.Count == 1 ? methods[0] : methods.FirstOrDefault(m => m.GetParameters().Length == action.Params.Length); + + if (method == null) + { + Debug.LogMessage(LogEventLevel.Warning, + "Unable to find method with name {methodName} and that matches parameters {@parameters}", null, action.MethodName, + action.Params); + return; + } + var mParams = method.GetParameters(); + + var convertedParams = mParams + .Select((p, i) => ConvertType(action.Params[i], p.ParameterType)) + .ToArray(); + + try + { + Debug.LogMessage(LogEventLevel.Verbose, "Calling method {methodName} on device {deviceKey} with {@params}", null, method.Name, action.DeviceKey, action.Params); + var result = method.Invoke(obj, convertedParams); + + // If the method returns a Task, await it + if (result is Task task) + { + await task; + } + // If the method returns a Task, await it + else if (result != null && result.GetType().IsGenericType && result.GetType().GetGenericTypeDefinition() == typeof(Task<>)) + { + await (Task)result; + } + } + catch (Exception e) + { + Debug.LogMessage(e, "Error invoking method {methodName} on device {deviceKey}", null, method.Name, action.DeviceKey); + } + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Unable to call method with name {methodName} with {@parameters}", null, action.MethodName, action.Params); + } + } + + private static object ConvertType(object value, Type conversionType) + { + if (!conversionType.IsEnum) + { + return Convert.ChangeType(value, conversionType, System.Globalization.CultureInfo.InvariantCulture); + } + + var stringValue = Convert.ToString(value); + + if (String.IsNullOrEmpty(stringValue)) + { + throw new InvalidCastException( + String.Format("{0} cannot be converted to a string prior to conversion to enum")); + } + return Enum.Parse(conversionType, stringValue, true); + } + + /// + /// Gets the properties on a device + /// + /// The path to the device object. + /// A JSON-formatted string representing the properties of the device. + public static string GetProperties(string deviceObjectPath) + { + var obj = FindObjectOnPath(deviceObjectPath); + if (obj == null) + return "{ \"error\":\"No Device\"}"; + + Type t = obj.GetType(); + // get the properties and set them into a new collection of NameType wrappers + var props = t.GetProperties().Select(p => new PropertyNameType(p, obj)); + return JsonConvert.SerializeObject(props, Formatting.Indented); + } + + /// + /// Gets a property from a device path by name + /// + /// The path to the device object. + /// The name of the property to retrieve. + /// The value of the property, or a JSON-formatted error string if the device or property is not found. + public static object GetPropertyByName(string deviceObjectPath, string propertyName) + { + var dev = FindObjectOnPath(deviceObjectPath); + if (dev == null) + return "{ \"error\":\"No Device\"}"; + + object prop = dev.GetType().GetProperty(propertyName).GetValue(dev, null); + + // var prop = t.GetProperty(propertyName); + if (prop != null) + { + return prop; + } + else + { + Debug.LogMessage(LogEventLevel.Debug, "Unable to find Property: {0} on Device with path: {1}", propertyName, deviceObjectPath); + return null; + } + } + + /// + /// Gets the methods on a device + /// + /// The path to the device object. + /// A JSON-formatted string representing the methods of the device. + public static string GetMethods(string deviceObjectPath) + { + var obj = FindObjectOnPath(deviceObjectPath); + if (obj == null) + return "{ \"error\":\"No Device\"}"; + + // Package up method names using helper objects + Type t = obj.GetType(); + var methods = t.GetMethods() + .Where(m => !m.IsSpecialName) + .Select(p => new MethodNameParams(p)); + return JsonConvert.SerializeObject(methods, Formatting.Indented); + } + + /// + /// Gets the API methods on a device, which are the methods marked with the ApiAttribute. + /// These are the methods intended to be called through the JSON API. + /// This allows for hiding certain methods from the API if desired. + /// + /// The path to the device object. + /// A JSON-formatted string representing the API methods of the device. + public static string GetApiMethods(string deviceObjectPath) + { + var obj = FindObjectOnPath(deviceObjectPath); + if (obj == null) + return "{ \"error\":\"No Device\"}"; + + // Package up method names using helper objects + Type t = obj.GetType(); + var methods = t.GetMethods() + .Where(m => !m.IsSpecialName) + .Where(m => m.GetCustomAttributes(typeof(ApiAttribute), true).Any()) + .Select(p => new MethodNameParams(p)); + return JsonConvert.SerializeObject(methods, Formatting.Indented); + } + + + /// + /// Walks down a dotted object path, starting with a Device, and returns the object + /// at the end of the path + /// + public static object FindObjectOnPath(string deviceObjectPath) + { + var path = deviceObjectPath.Split('.'); + + var dev = DeviceManager.GetDeviceForKey(path[0]); + if (dev == null) + { + Debug.LogMessage(LogEventLevel.Information, "Device {0} not found", path[0]); + return null; + } + + // loop through any dotted properties + object obj = dev; + if (path.Length > 1) + { + for (int i = 1; i < path.Length; i++) + { + var objName = path[i]; + string indexStr = null; + var indexOpen = objName.IndexOf('['); + if (indexOpen != -1) + { + var indexClose = objName.IndexOf(']'); + if (indexClose == -1) + { + Debug.LogMessage(LogEventLevel.Information, dev, "ERROR Unmatched index brackets"); return null; } + // Get the index and strip quotes if any + indexStr = objName.Substring(indexOpen + 1, indexClose - indexOpen - 1).Replace("\"", ""); + objName = objName.Substring(0, indexOpen); + Debug.LogMessage(LogEventLevel.Information, dev, " Checking for collection '{0}', index '{1}'", objName, indexStr); + } + + Type oType = obj.GetType(); + var prop = oType.GetProperty(objName); + if (prop == null) + { + Debug.LogMessage(LogEventLevel.Information, dev, "Property {0} not found on {1}", objName, path[i - 1]); + return null; + } + // if there's an index, try to get the property + if (indexStr != null) + { + if (!typeof(ICollection).IsAssignableFrom(prop.PropertyType)) + { + Debug.LogMessage(LogEventLevel.Information, dev, "Property {0} is not collection", objName); + return null; + } + var collection = prop.GetValue(obj, null) as ICollection; + // Get the indexed items "property" + var indexedPropInfo = prop.PropertyType.GetProperty("Item"); + // These are the parameters for the indexing. Only care about one + var indexParams = indexedPropInfo.GetIndexParameters(); + if (indexParams.Length > 0) + { + Debug.LogMessage(LogEventLevel.Information, " Indexed, param type: {0}", indexParams[0].ParameterType.Name); + var properParam = Convert.ChangeType(indexStr, indexParams[0].ParameterType, + System.Globalization.CultureInfo.InvariantCulture); + try + { + obj = indexedPropInfo.GetValue(collection, new object[] { properParam }); + } + // if the index is bad, catch it here. + catch (TargetInvocationException e) + { + if (e.InnerException is ArgumentOutOfRangeException) + Debug.LogMessage(LogEventLevel.Information, " Index Out of range"); + else if (e.InnerException is KeyNotFoundException) + Debug.LogMessage(LogEventLevel.Information, " Key not found"); + return null; + } + } + } else - return null; + obj = prop.GetValue(obj, null); } } - - /// - /// Gets or sets the CanRead - /// - public bool CanRead { get { return PropInfo.CanRead; } } - - /// - /// Gets or sets the CanWrite - /// - public bool CanWrite { get { return PropInfo.CanWrite; } } - - /// - /// PropertyNameType constructor - /// - /// property info - /// parent object - public PropertyNameType(PropertyInfo info, object parent) - { - PropInfo = info; - Parent = parent; - } + return obj; } /// - /// Represents a MethodNameParams + /// Sets a property on an object. /// - public class MethodNameParams + /// + /// + public static string SetProperty(string deviceObjectPath) { - /// - /// Gets or sets the MethodInfo - /// - [JsonIgnore] - public MethodInfo MethodInfo { get; private set; } + throw new NotImplementedException("This could be really useful. Finish it please"); - /// - /// Gets or sets the Name - /// - public string Name { get { return MethodInfo.Name; } } + //var obj = FindObjectOnPath(deviceObjectPath); + //if (obj == null) + // return "{\"error\":\"No object found\"}"; - /// - /// Gets or sets the Params - /// - public IEnumerable Params + //Type t = obj.GetType(); + + + //// get the properties and set them into a new collection of NameType wrappers + //var props = t.GetProperties().Select(p => new PropertyNameType(p, obj)); + //return JsonConvert.SerializeObject(props, Formatting.Indented); + } + + +} + +/// +/// Attribute to mark methods as part of the JSON API. Only methods marked with this attribute will be returned by GetApiMethods and intended to be called through the JSON API. +/// +public class DeviceActionWrapper +{ + /// + /// The key of the device to call the method on + /// + public string DeviceKey { get; set; } + + /// + /// The name of the method to call + /// + public string MethodName { get; set; } + + /// + /// The parameters to pass to the method. This should be an array of objects matching the parameters of the method being called. If the method has no parameters, this can be omitted or set to null. + /// + public object[] Params { get; set; } +} + +/// +/// Helper class for serializing properties with their name, type, and value, and whether they can be read or written to. +/// +public class PropertyNameType +{ + private object Parent; + + /// + /// The PropertyInfo for the property being represented. This is ignored for JSON serialization. + /// + [JsonIgnore] + public PropertyInfo PropInfo { get; private set; } + + /// + /// The name of the property + /// + public string Name { get { return PropInfo.Name; } } + + /// + /// The type of the property + /// + public string Type { get { return PropInfo.PropertyType.Name; } } + + /// + /// The value of the property, or null if the property cannot be read or an error occurs when trying to read it. + /// + public string Value + { + get { - get + if (PropInfo.CanRead) { - return MethodInfo.GetParameters().Select(p => - new NameType { Name = p.Name, Type = p.ParameterType.Name }); + try + { + return PropInfo.GetValue(Parent, null).ToString(); + } + catch (Exception) + { + return null; + } } + else + return null; } + } - /// - /// MethodNameParams constructor - /// - /// method info - public MethodNameParams(MethodInfo info) + /// + /// Indicates whether the property can be read from + /// + public bool CanRead { get { return PropInfo.CanRead; } } + /// + /// Indicates whether the property can be written to + /// + public bool CanWrite { get { return PropInfo.CanWrite; } } + + + /// + /// Constructor + /// + /// + /// + public PropertyNameType(PropertyInfo info, object parent) + { + PropInfo = info; + Parent = parent; + } +} + +/// +/// Helper class for serializing methods with their name and parameters. The MethodInfo is ignored for JSON serialization. +/// +public class MethodNameParams +{ + /// + /// The MethodInfo for the method being represented. This is ignored for JSON serialization. + /// + [JsonIgnore] + public MethodInfo MethodInfo { get; private set; } + + /// + /// The name of the method + /// + public string Name { get { return MethodInfo.Name; } } + + /// + /// The parameters of the method, represented as an array of NameType objects with the parameter name and type. + /// + public IEnumerable Params + { + get { - MethodInfo = info; + return MethodInfo.GetParameters().Select(p => + new NameType { Name = p.Name, Type = p.ParameterType.Name }); } } /// - /// Represents a NameType + /// Constructor /// - public class NameType + /// + public MethodNameParams(MethodInfo info) { - /// - /// Gets or sets the Name - /// - public string Name { get; set; } - /// - /// Gets or sets the Type - /// - public string Type { get; set; } + MethodInfo = info; } +} + +/// +/// Helper class for serializing a name and type pair, used for method parameters in MethodNameParams. +/// +public class NameType +{ + /// + /// The name of the parameter + /// + public string Name { get; set; } /// - /// Represents a ApiAttribute + /// The type of the parameter /// - [AttributeUsage(AttributeTargets.All)] - public class ApiAttribute : Attribute - { + public string Type { get; set; } +} + +/// +/// Attribute to mark methods as part of the JSON API. Only methods marked with this attribute will be returned by GetApiMethods and intended to be called through the JSON API. +/// +[AttributeUsage(AttributeTargets.All)] +public class ApiAttribute : Attribute +{ - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/DisplayUiConstants.cs b/src/PepperDash.Essentials.Core/Devices/DisplayUiConstants.cs index 1c6ae244..39e7067b 100644 --- a/src/PepperDash.Essentials.Core/Devices/DisplayUiConstants.cs +++ b/src/PepperDash.Essentials.Core/Devices/DisplayUiConstants.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Integers that represent the "source type number" for given sources. /// Primarily used by the UI to calculate subpage join offsets @@ -62,5 +62,4 @@ namespace PepperDash.Essentials.Core /// TypeNoControls constant /// public const uint TypeNoControls = 49; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs index 0b1a5ff7..23b06e9a 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs @@ -1,31 +1,17 @@ using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Essentials.Core.Bridges; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public abstract class EssentialsBridgeableDevice:EssentialsDevice, IBridgeAdvanced { - /// - /// Base class for devices that can be bridged to an EISC API. - /// - public abstract class EssentialsBridgeableDevice : EssentialsDevice, IBridgeAdvanced + protected EssentialsBridgeableDevice(string key) : base(key) { - /// - /// Initializes a new instance of the class with the specified key. - /// - /// The unique key for the device. - protected EssentialsBridgeableDevice(string key) : base(key) - { - } - - /// - /// Initializes a new instance of the class with the specified key and name. - /// - /// The unique key for the device. - /// The display name for the device. - protected EssentialsBridgeableDevice(string key, string name) : base(key, name) - { - } - - /// - public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); } + + protected EssentialsBridgeableDevice(string key, string name) : base(key, name) + { + } + + public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs index 41b46c29..3b4f3789 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs @@ -5,185 +5,225 @@ using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines the basic needs for an EssentialsDevice to enable it to be build by an IDeviceFactory class +/// +[Description("The base Essentials Device Class")] +public abstract class EssentialsDevice : Device { /// - /// Defines the basic needs for an EssentialsDevice to enable it to be build by an IDeviceFactory class + /// Event that fires when the device has completed initialization. This is useful for any setup that needs to occur after all devices have been activated. /// - [Description("The base Essentials Device Class")] - public abstract class EssentialsDevice : Device + public event EventHandler Initialized; + + private bool _isInitialized; + + /// + /// Indicates whether the device has completed initialization. Initialization occurs after all devices have been activated, and is triggered by the DeviceManager.AllDevicesActivated event. This property can be used to determine when it is safe to perform actions that require all devices to be active. + /// + public bool IsInitialized { - /// - /// Event raised when the device is initialized. - /// - public event EventHandler Initialized; - - private bool _isInitialized; - - /// - /// Gets a value indicating whether the device is initialized. - /// - public bool IsInitialized + get { return _isInitialized; } + private set { - get { return _isInitialized; } - private set + if (_isInitialized == value) return; + + _isInitialized = value; + + if (_isInitialized) { - if (_isInitialized == value) return; - - _isInitialized = value; - - if (_isInitialized) - { - Initialized?.Invoke(this, new EventArgs()); - } + Initialized?.Invoke(this, new EventArgs()); } } - - /// - /// Initializes a new instance of the EssentialsDevice class. - /// - /// The unique identifier for the device. - protected EssentialsDevice(string key) - : base(key) - { - SubscribeToActivateComplete(); - } - - /// - /// Initializes a new instance of the EssentialsDevice class. - /// - /// The unique identifier for the device. - /// The name of the device. - protected EssentialsDevice(string key, string name) - : base(key, name) - { - SubscribeToActivateComplete(); - } - - private void SubscribeToActivateComplete() - { - DeviceManager.AllDevicesActivated += DeviceManagerOnAllDevicesActivated; - } - - private void DeviceManagerOnAllDevicesActivated(object sender, EventArgs eventArgs) - { - Task.Run(() => - { - try - { - Initialize(); - - IsInitialized = true; - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Error, this, "Exception initializing device: {0}", ex.Message); - Debug.LogMessage(LogEventLevel.Debug, this, "Stack Trace: {0}", ex.StackTrace); - } - }); - } - - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() - { - CreateMobileControlMessengers(); - - return base.CustomActivate(); - } - - /// - /// Override this method to build and create custom Mobile Control Messengers during the Activation phase - /// - protected virtual void CreateMobileControlMessengers() - { - - } - } - - [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] - public class DescriptionAttribute : Attribute - { - private string _Description; - - public DescriptionAttribute(string description) - { - //Debug.LogMessage(LogEventLevel.Verbose, "Setting Description: {0}", description); - _Description = description; - } - - public string Description - { - get { return _Description; } - } - } - - [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] - public class ConfigSnippetAttribute : Attribute - { - private string _ConfigSnippet; - - public ConfigSnippetAttribute(string configSnippet) - { - //Debug.LogMessage(LogEventLevel.Verbose, "Setting Config Snippet {0}", configSnippet); - _ConfigSnippet = configSnippet; - } - - public string ConfigSnippet - { - get { return _ConfigSnippet; } - } } /// - /// Represents a factory for creating processor extension devices. + /// Constructor /// - /// The type of the processor extension device. - public abstract class ProcessorExtensionDeviceFactory : IProcessorExtensionDeviceFactory where T : EssentialsDevice + /// + protected EssentialsDevice(string key) + : base(key) { - #region IProcessorExtensionDeviceFactory Members - - /// - /// A list of strings that can be used in the type property of a DeviceConfig object to build an instance of this device - /// - public List TypeNames { get; protected set; } - - /// - /// Loads an item to the ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods dictionary for each entry in the TypeNames list - /// - public void LoadFactories() - { - foreach (var typeName in TypeNames) - { - //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); - var descriptionAttribute = typeof(T).GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; - string description = descriptionAttribute[0].Description; - var snippetAttribute = typeof(T).GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; - ProcessorExtensionDeviceFactory.AddFactoryForType(typeName.ToLower(), description, typeof(T), BuildDevice); - } - } - - /// - /// The method that will build the device - /// - /// The device config - /// An instance of the device - public abstract EssentialsDevice BuildDevice(DeviceConfig dc); - - #endregion - + SubscribeToActivateComplete(); } /// - /// Devices the basic needs for a Device Factory + /// Constructor /// - public abstract class EssentialsPluginDeviceFactory : EssentialsDeviceFactory, IPluginDeviceFactory where T : EssentialsDevice + /// + /// + protected EssentialsDevice(string key, string name) + : base(key, name) { - /// - /// Specifies the minimum version of Essentials required for a plugin to run. Must use the format Major.Minor.Build (ex. "1.4.33") - /// - public string MinimumEssentialsFrameworkVersion { get; protected set; } + SubscribeToActivateComplete(); } + + private void SubscribeToActivateComplete() + { + DeviceManager.AllDevicesActivated += DeviceManagerOnAllDevicesActivated; + } + + private void DeviceManagerOnAllDevicesActivated(object sender, EventArgs eventArgs) + { + Task.Run(() => + { + try + { + Initialize(); + + IsInitialized = true; + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Error, this, "Exception initializing device: {0}", ex.Message); + Debug.LogMessage(LogEventLevel.Debug, this, "Stack Trace: {0}", ex.StackTrace); + } + }); + } + + /// + /// Override this method to perform any initialization that requires all devices to be activated. This method is called automatically after the DeviceManager.AllDevicesActivated event is fired, and should not be called directly. + /// + /// + public override bool CustomActivate() + { + CreateMobileControlMessengers(); + + return base.CustomActivate(); + } + + /// + /// Override this method to build and create custom Mobile Control Messengers during the Activation phase + /// + protected virtual void CreateMobileControlMessengers() + { + + } +} + +/// +/// Provides a description for a device that can be used in the UI and other areas where a human-readable description of the device is needed. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] +public class DescriptionAttribute : Attribute +{ + private string _Description; + + /// + /// Constructor + /// + /// + public DescriptionAttribute(string description) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Setting Description: {0}", description); + _Description = description; + } + + /// + /// Gets the description of the device + /// + public string Description + { + get { return _Description; } + } +} + +/// +/// Provides a configuration snippet for a device that can be used in the UI and other areas where a sample configuration of the device is needed. This is especially useful for plugin developers to provide users with an example of how to configure their device. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] +public class ConfigSnippetAttribute : Attribute +{ + private string _ConfigSnippet; + + /// + /// Constructor + /// + /// + public ConfigSnippetAttribute(string configSnippet) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Setting Config Snippet {0}", configSnippet); + _ConfigSnippet = configSnippet; + } + + /// + /// Gets the configuration snippet for the device + /// + public string ConfigSnippet + { + get { return _ConfigSnippet; } + } +} + +/// +/// Provides a base class for creating device factories specifically designed for processor extension development in the Essentials framework. This class implements the IProcessorExtensionDeviceFactory interface and provides a structure for defining type names and building devices based on a DeviceConfig object. +/// +/// The type of device that this factory creates. Must derive from . +public abstract class ProcessorExtensionDeviceFactory : IProcessorExtensionDeviceFactory where T : EssentialsDevice +{ + #region IProcessorExtensionDeviceFactory Members + + /// + /// A list of strings that can be used in the type property of a DeviceConfig object to build an instance of this device + /// + public List TypeNames { get; protected set; } + + /// + /// Loads an item to the ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods dictionary for each entry in the TypeNames list + /// + public void LoadFactories() + { + foreach (var typeName in TypeNames) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = typeof(T).GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = typeof(T).GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + ProcessorExtensionDeviceFactory.AddFactoryForType(typeName.ToLower(), description, typeof(T), BuildDevice); + } + } + + /// + /// The method that will build the device + /// + /// The device config + /// An instance of the device + public abstract EssentialsDevice BuildDevice(DeviceConfig dc); + + #endregion + +} + +/// +/// Devices the basic needs for a Device Factory +/// +public abstract class EssentialsPluginDeviceFactory : EssentialsDeviceFactory, IPluginDeviceFactory where T : EssentialsDevice +{ + /// + /// Specifies the minimum version of Essentials required for a plugin to run. Must use the format Major.Minor.Build (ex. "1.4.33") + /// + public string MinimumEssentialsFrameworkVersion { get; protected set; } +} + +/// +/// Provides a base class for creating device factories specifically designed for plugin development in the Essentials +/// framework. +/// +/// This class is intended to be used by developers creating plugins for the Essentials framework. It +/// includes properties to specify the minimum required Essentials framework version and to track framework versions +/// used during development. +/// The type of device that this factory creates. Must derive from . +public abstract class EssentialsPluginDevelopmentDeviceFactory : EssentialsDeviceFactory, IPluginDevelopmentDeviceFactory where T : EssentialsDevice +{ + /// + /// Specifies the minimum version of Essentials required for a plugin to run. Must use the format Major.Minor.Build (ex. "1.4.33") + /// + public string MinimumEssentialsFrameworkVersion { get; protected set; } + + /// + /// A list of Essentials framework versions that were used during the development of the plugin. This can be used to track which versions of the framework the plugin is compatible with, and can be helpful for users when determining whether a plugin will work with their current version of the Essentials framework. + /// + public List DevelopmentEssentialsFrameworkVersions { get; protected set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs index bf635c0c..3f105628 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs @@ -2,31 +2,29 @@ using System.Collections.Generic; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Devices the basic needs for a Device Factory +/// +public abstract class EssentialsDeviceFactory : IDeviceFactory where T:EssentialsDevice { + #region IDeviceFactory Members + + /// + public Type FactoryType => typeof(T); + /// - /// Devices the basic needs for a Device Factory + /// A list of strings that can be used in the type property of a DeviceConfig object to build an instance of this device /// - public abstract class EssentialsDeviceFactory : IDeviceFactory where T:EssentialsDevice - { - #region IDeviceFactory Members + public List TypeNames { get; protected set; } - /// - public Type FactoryType => typeof(T); - - /// - /// A list of strings that can be used in the type property of a DeviceConfig object to build an instance of this device - /// - public List TypeNames { get; protected set; } - - /// - /// The method that will build the device - /// - /// The device config - /// An instance of the device - public abstract EssentialsDevice BuildDevice(DeviceConfig dc); - - #endregion - } + /// + /// The method that will build the device + /// + /// The device config + /// An instance of the device + public abstract EssentialsDevice BuildDevice(DeviceConfig dc); + #endregion } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/GenericIRController.cs b/src/PepperDash.Essentials.Core/Devices/GenericIRController.cs index a55235e6..77bb4fc3 100644 --- a/src/PepperDash.Essentials.Core/Devices/GenericIRController.cs +++ b/src/PepperDash.Essentials.Core/Devices/GenericIRController.cs @@ -11,84 +11,68 @@ using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Bridges.JoinMaps; using Serilog.Events; -namespace PepperDash.Essentials.Core.Devices +namespace PepperDash.Essentials.Core.Devices; + +public class GenericIrController: EssentialsBridgeableDevice { - /// - /// Represents a GenericIrController - /// - public class GenericIrController: EssentialsBridgeableDevice + //data storage for bridging + private BasicTriList _trilist; + private uint _joinStart; + private string _joinMapKey; + private EiscApiAdvanced _bridge; + + private readonly IrOutputPortController _port; + + public string[] IrCommands {get { return _port.IrFileCommands; }} + + public GenericIrController(string key, string name, IrOutputPortController irPort) : base(key, name) { - //data storage for bridging - private BasicTriList _trilist; - private uint _joinStart; - private string _joinMapKey; - private EiscApiAdvanced _bridge; - - private readonly IrOutputPortController _port; - - /// - /// Gets or sets the IrCommands - /// - public string[] IrCommands {get { return _port.IrFileCommands; }} - - /// - /// Constructor - /// - /// key for the device - /// name of the device - /// IR output port controller - public GenericIrController(string key, string name, IrOutputPortController irPort) : base(key, name) + _port = irPort; + if (_port == null) { - _port = irPort; - if (_port == null) - { - Debug.LogMessage(LogEventLevel.Information, this, "IR Port is null, device will not function"); - return; - } - DeviceManager.AddDevice(_port); + Debug.LogMessage(LogEventLevel.Information, this, "IR Port is null, device will not function"); + return; + } + DeviceManager.AddDevice(_port); - _port.DriverLoaded.OutputChange += DriverLoadedOnOutputChange; + _port.DriverLoaded.OutputChange += DriverLoadedOnOutputChange; + } + + private void DriverLoadedOnOutputChange(object sender, FeedbackEventArgs args) + { + if (!args.BoolValue) + { + return; } - private void DriverLoadedOnOutputChange(object sender, FeedbackEventArgs args) + if (_trilist == null || _bridge == null) { - if (!args.BoolValue) - { - return; - } - - if (_trilist == null || _bridge == null) - { - return; - } - - LinkToApi(_trilist, _joinStart, _joinMapKey, _bridge); + return; } - #region Overrides of EssentialsBridgeableDevice + LinkToApi(_trilist, _joinStart, _joinMapKey, _bridge); + } - /// - /// LinkToApi method - /// - /// - public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + #region Overrides of EssentialsBridgeableDevice + + public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + //if driver isn't loaded yet, store the variables until it is loaded, then call the LinkToApi method again + if (!_port.DriverIsLoaded) { - //if driver isn't loaded yet, store the variables until it is loaded, then call the LinkToApi method again - if (!_port.DriverIsLoaded) - { - _trilist = trilist; - _joinStart = joinStart; - _joinMapKey = joinMapKey; - _bridge = bridge; - return; - } + _trilist = trilist; + _joinStart = joinStart; + _joinMapKey = joinMapKey; + _bridge = bridge; + return; + } - var joinMap = new GenericIrControllerJoinMap(joinStart); + var joinMap = new GenericIrControllerJoinMap(joinStart); - var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); - if (!string.IsNullOrEmpty(joinMapSerialized)) - joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); if (_port.UseBridgeJoinMap) { @@ -150,56 +134,42 @@ namespace PepperDash.Essentials.Core.Devices } } - joinMap.PrintJoinMapInfo(); + joinMap.PrintJoinMapInfo(); - if (bridge != null) - { - bridge.AddJoinMap(Key, joinMap); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); - } - } - - #endregion - - /// - /// Press method - /// - public void Press(string command, bool pressRelease) + if (bridge != null) { - _port.PressRelease(command, pressRelease); + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); } } - /// - /// Represents a GenericIrControllerFactory - /// - public class GenericIrControllerFactory : EssentialsDeviceFactory + #endregion + + public void Press(string command, bool pressRelease) { - /// - /// Constructor - /// - public GenericIrControllerFactory() - { - TypeNames = new List {"genericIrController"}; - } - #region Overrides of EssentialsDeviceFactory - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic IR Controller Device"); - - var irPort = IRPortHelper.GetIrOutputPortController(dc); - - return new GenericIrController(dc.Key, dc.Name, irPort); - } - - #endregion + _port.PressRelease(command, pressRelease); } +} + +public class GenericIrControllerFactory : EssentialsDeviceFactory +{ + public GenericIrControllerFactory() + { + TypeNames = new List {"genericIrController"}; + } + #region Overrides of EssentialsDeviceFactory + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic IR Controller Device"); + + var irPort = IRPortHelper.GetIrOutputPortController(dc); + + return new GenericIrController(dc.Key, dc.Name, irPort); + } + + #endregion } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/GenericMonitoredTcpDevice.cs b/src/PepperDash.Essentials.Core/Devices/GenericMonitoredTcpDevice.cs index 148a9cac..f43bf8a3 100644 --- a/src/PepperDash.Essentials.Core/Devices/GenericMonitoredTcpDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/GenericMonitoredTcpDevice.cs @@ -8,10 +8,10 @@ using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.Devices -{ +namespace PepperDash.Essentials.Core.Devices; + /// - /// Represents a GenericCommunicationMonitoredDevice + /// A device that monitors the communication status of a communication client. Useful for monitoring TCP, serial, or telnet clients. /// public class GenericCommunicationMonitoredDevice : Device, ICommunicationMonitor { @@ -77,5 +77,4 @@ namespace PepperDash.Essentials.Core.Devices CommunicationMonitor.Stop(); return true; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/IAttachVideoStatusExtensions.cs b/src/PepperDash.Essentials.Core/Devices/IAttachVideoStatusExtensions.cs index 83914f6f..4d28fa18 100644 --- a/src/PepperDash.Essentials.Core/Devices/IAttachVideoStatusExtensions.cs +++ b/src/PepperDash.Essentials.Core/Devices/IAttachVideoStatusExtensions.cs @@ -4,42 +4,41 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Interface for devices that can be attached to a port that provides video status information. Provides an extension method for getting that status information. +/// +public static class IAttachVideoStatusExtensions { /// - /// IAttachVideoStatusExtensions class + /// Gets the VideoStatusOutputs for the device /// - public static class IAttachVideoStatusExtensions + /// + /// Attached VideoStatusOutputs or the default if none attached + public static VideoStatusOutputs GetVideoStatuses(this IAttachVideoStatus attachedDev) { - /// - /// Gets the VideoStatusOutputs for the device - /// - /// - /// Attached VideoStatusOutputs or the default if none attached - public static VideoStatusOutputs GetVideoStatuses(this IAttachVideoStatus attachedDev) + // See if this device is connected to a status-providing port + var tl = TieLineCollection.Default.FirstOrDefault(t => + t.SourcePort.ParentDevice == attachedDev + && t.DestinationPort is RoutingInputPortWithVideoStatuses); + if (tl != null) { - // See if this device is connected to a status-providing port - var tl = TieLineCollection.Default.FirstOrDefault(t => - t.SourcePort.ParentDevice == attachedDev - && t.DestinationPort is RoutingInputPortWithVideoStatuses); - if (tl != null) - { - // if so, and it's got status, return it -- or null - var port = tl.DestinationPort as RoutingInputPortWithVideoStatuses; - if (port != null) - return port.VideoStatus; - } - return VideoStatusOutputs.NoStatus; + // if so, and it's got status, return it -- or null + var port = tl.DestinationPort as RoutingInputPortWithVideoStatuses; + if (port != null) + return port.VideoStatus; } + return VideoStatusOutputs.NoStatus; + } - /// - /// HasVideoStatuses method - /// - public static bool HasVideoStatuses(this IAttachVideoStatus attachedDev) - { - return TieLineCollection.Default.FirstOrDefault(t => - t.SourcePort.ParentDevice == attachedDev - && t.DestinationPort is RoutingInputPortWithVideoStatuses) != null; - } + /// + /// HasVideoStatuses method + /// + public static bool HasVideoStatuses(this IAttachVideoStatus attachedDev) + { + return TieLineCollection.Default.FirstOrDefault(t => + t.SourcePort.ParentDevice == attachedDev + && t.DestinationPort is RoutingInputPortWithVideoStatuses) != null; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/IHasFeedbacks.cs b/src/PepperDash.Essentials.Core/Devices/IHasFeedbacks.cs index 330538e0..9752d0e7 100644 --- a/src/PepperDash.Essentials.Core/Devices/IHasFeedbacks.cs +++ b/src/PepperDash.Essentials.Core/Devices/IHasFeedbacks.cs @@ -48,42 +48,40 @@ namespace PepperDash.Essentials.Core /// public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates) { + Type t = source.GetType(); + // get the properties and set them into a new collection of NameType wrappers + var props = t.GetProperties().Select(p => new PropertyNameType(p, t)); + var feedbacks = source.Feedbacks; if (feedbacks == null || feedbacks.Count == 0) { - CrestronConsole.ConsoleCommandResponse("No available feedbacks\r\n"); - return; - } - - CrestronConsole.ConsoleCommandResponse("Available feedbacks:\r\n"); - - // Sort feedbacks by type first, then by key - var sortedFeedbacks = feedbacks.OrderBy(f => GetFeedbackTypeName(f)).ThenBy(f => string.IsNullOrEmpty(f.Key) ? "" : f.Key); - - foreach (var feedback in sortedFeedbacks) - { - string value = ""; - string type = ""; - if (getCurrentStates) + Debug.LogMessage(LogEventLevel.Information, source, "\n\nAvailable feedbacks:"); + foreach (var f in feedbacks) { - if (feedback is BoolFeedback) + string val = ""; + string type = ""; + if (getCurrentStates) { - value = feedback.BoolValue.ToString(); - type = "boolean"; - } - else if (feedback is IntFeedback) - { - value = feedback.IntValue.ToString(); - type = "integer"; - } - else if (feedback is StringFeedback) - { - value = feedback.StringValue; - type = "string"; + if (f is BoolFeedback) + { + val = f.BoolValue.ToString(); + type = "boolean"; + } + else if (f is IntFeedback) + { + val = f.IntValue.ToString(); + type = "integer"; + } + else if (f is StringFeedback) + { + val = f.StringValue; + type = "string"; + } } + Debug.LogMessage(LogEventLevel.Information, "{0,-12} {1, -25} {2}", type, + (string.IsNullOrEmpty(f.Key) ? "-no key-" : f.Key), val); } - CrestronConsole.ConsoleCommandResponse($" {type,-12} {(string.IsNullOrEmpty(feedback.Key) ? "-no key-" : feedback.Key),-25} {value}\r\n"); } } } diff --git a/src/PepperDash.Essentials.Core/Devices/IProjectorInterfaces.cs b/src/PepperDash.Essentials.Core/Devices/IProjectorInterfaces.cs index d6473e64..d43c6e3b 100644 --- a/src/PepperDash.Essentials.Core/Devices/IProjectorInterfaces.cs +++ b/src/PepperDash.Essentials.Core/Devices/IProjectorInterfaces.cs @@ -1,41 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; +namespace PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core +/// +/// Defines the contract for IBasicVideoMute +/// +public interface IBasicVideoMute { /// - /// Defines the contract for IBasicVideoMute + /// Toggles the video mute /// - public interface IBasicVideoMute - { - /// - /// Toggles the video mute - /// - void VideoMuteToggle(); - } + void VideoMuteToggle(); +} + +/// +/// Defines the contract for IBasicVideoMuteWithFeedback +/// +public interface IBasicVideoMuteWithFeedback : IBasicVideoMute +{ + /// + /// Gets the VideoMuteIsOn feedback + /// + BoolFeedback VideoMuteIsOn { get; } /// - /// Defines the contract for IBasicVideoMuteWithFeedback + /// Sets the video mute on /// - public interface IBasicVideoMuteWithFeedback : IBasicVideoMute - { - /// - /// Gets the VideoMuteIsOn feedback - /// - BoolFeedback VideoMuteIsOn { get; } + void VideoMuteOn(); - /// - /// Sets the video mute on - /// - void VideoMuteOn(); + /// + /// Sets the video mute off + /// + void VideoMuteOff(); - /// - /// Sets the video mute off - /// - void VideoMuteOff(); - - } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Core/Devices/IReconfigurableDevice.cs b/src/PepperDash.Essentials.Core/Devices/IReconfigurableDevice.cs index 245c0a5c..7dca4f32 100644 --- a/src/PepperDash.Essentials.Core/Devices/IReconfigurableDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/IReconfigurableDevice.cs @@ -6,27 +6,27 @@ using Crestron.SimplSharp; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials.Core.Devices +namespace PepperDash.Essentials.Core.Devices; + + +/// +/// Defines the contract for IReconfigurableDevice +/// +public interface IReconfigurableDevice { /// - /// Defines the contract for IReconfigurableDevice + /// Event fired when the configuration changes /// - public interface IReconfigurableDevice - { - /// - /// Event fired when the configuration changes - /// - event EventHandler ConfigChanged; + event EventHandler ConfigChanged; - /// - /// Gets the current DeviceConfig - /// - DeviceConfig Config { get; } + /// + /// Gets the current DeviceConfig + /// + DeviceConfig Config { get; } - /// - /// Sets the DeviceConfig - /// - /// config to set - void SetConfig(DeviceConfig config); - } -} \ No newline at end of file + /// + /// Sets the DeviceConfig + /// + /// config to set + void SetConfig(DeviceConfig config); +} diff --git a/src/PepperDash.Essentials.Core/Devices/IUsageTracking.cs b/src/PepperDash.Essentials.Core/Devices/IUsageTracking.cs index 7edce646..4a45f0cc 100644 --- a/src/PepperDash.Essentials.Core/Devices/IUsageTracking.cs +++ b/src/PepperDash.Essentials.Core/Devices/IUsageTracking.cs @@ -6,143 +6,99 @@ using Crestron.SimplSharp; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public interface IUsageTracking { - /// - /// Defines the contract for IUsageTracking - /// - public interface IUsageTracking + UsageTracking UsageTracker { get; set; } +} + +//public static class IUsageTrackingExtensions +//{ +// public static void EnableUsageTracker(this IUsageTracking device) +// { +// device.UsageTracker = new UsageTracking(); +// } +//} + +public class UsageTracking +{ + public event EventHandler DeviceUsageEnded; + + public InUseTracking InUseTracker { get; protected set; } + + public bool UsageIsTracked { get; set; } + + public bool UsageTrackingStarted { get; protected set; } + public DateTime UsageStartTime { get; protected set; } + public DateTime UsageEndTime { get; protected set; } + + public Device Parent { get; private set; } + + public UsageTracking(Device parent) { - /// - /// Gets or sets the UsageTracker - /// - UsageTracking UsageTracker { get; set; } + Parent = parent; + + InUseTracker = new InUseTracking(); + + InUseTracker.InUseFeedback.OutputChange += InUseFeedback_OutputChange; //new EventHandler(); + } + + void InUseFeedback_OutputChange(object sender, EventArgs e) + { + if(InUseTracker.InUseFeedback.BoolValue) + { + StartDeviceUsage(); + } + else + { + EndDeviceUsage(); + } } - //public static class IUsageTrackingExtensions - //{ - // public static void EnableUsageTracker(this IUsageTracking device) - // { - // device.UsageTracker = new UsageTracking(); - // } - //} /// - /// Represents a UsageTracking + /// Stores the usage start time /// - public class UsageTracking + public void StartDeviceUsage() { - /// - /// Event fired when device usage ends - /// - public event EventHandler DeviceUsageEnded; + UsageTrackingStarted = true; + UsageStartTime = DateTime.Now; + } - /// - /// Gets or sets the InUseTracker - /// - public InUseTracking InUseTracker { get; protected set; } - - /// - /// Gets or sets the UsageIsTracked - /// - public bool UsageIsTracked { get; set; } - - /// - /// Gets or sets the UsageTrackingStarted - /// - public bool UsageTrackingStarted { get; protected set; } - /// - /// Gets or sets the UsageStartTime - /// - public DateTime UsageStartTime { get; protected set; } - /// - /// Gets or sets the UsageEndTime - /// - public DateTime UsageEndTime { get; protected set; } - - /// - /// Gets or sets the Parent - /// - public Device Parent { get; private set; } - - /// - /// Constructor for UsageTracking class - /// - /// The parent device - public UsageTracking(Device parent) + /// + /// Calculates the difference between the usage start and end times, gets the total minutes used and fires an event to pass that info to a consumer + /// + public void EndDeviceUsage() + { + try { - Parent = parent; - - InUseTracker = new InUseTracking(); + UsageTrackingStarted = false; - InUseTracker.InUseFeedback.OutputChange += InUseFeedback_OutputChange; //new EventHandler(); - } + UsageEndTime = DateTime.Now; - void InUseFeedback_OutputChange(object sender, EventArgs e) - { - if(InUseTracker.InUseFeedback.BoolValue) + if (UsageStartTime != null) { - StartDeviceUsage(); - } - else - { - EndDeviceUsage(); - } - } + var timeUsed = UsageEndTime - UsageStartTime; + var handler = DeviceUsageEnded; - /// - /// StartDeviceUsage method - /// - public void StartDeviceUsage() - { - UsageTrackingStarted = true; - UsageStartTime = DateTime.Now; - } - - /// - /// Calculates the difference between the usage start and end times, gets the total minutes used and fires an event to pass that info to a consumer - /// - public void EndDeviceUsage() - { - try - { - UsageTrackingStarted = false; - - UsageEndTime = DateTime.Now; - - if (UsageStartTime != null) + if (handler != null) { - var timeUsed = UsageEndTime - UsageStartTime; - - var handler = DeviceUsageEnded; - - if (handler != null) - { - Debug.LogMessage(LogEventLevel.Debug, "Device Usage Ended for: {0} at {1}. In use for {2} minutes.", Parent.Name, UsageEndTime, timeUsed.Minutes); - handler(this, new DeviceUsageEventArgs() { UsageEndTime = UsageEndTime, MinutesUsed = timeUsed.Minutes }); - } + Debug.LogMessage(LogEventLevel.Debug, "Device Usage Ended for: {0} at {1}. In use for {2} minutes.", Parent.Name, UsageEndTime, timeUsed.Minutes); + handler(this, new DeviceUsageEventArgs() { UsageEndTime = UsageEndTime, MinutesUsed = timeUsed.Minutes }); } } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Debug, "Error ending device usage: {0}", e); - } + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Debug, "Error ending device usage: {0}", e); } } +} - /// - /// Represents a DeviceUsageEventArgs - /// - public class DeviceUsageEventArgs : EventArgs - { - /// - /// Gets or sets the UsageEndTime - /// - public DateTime UsageEndTime { get; set; } - /// - /// Gets or sets the MinutesUsed - /// - public int MinutesUsed { get; set; } - } +public class DeviceUsageEventArgs : EventArgs +{ + public DateTime UsageEndTime { get; set; } + public int MinutesUsed { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/IrOutputPortController.cs b/src/PepperDash.Essentials.Core/Devices/IrOutputPortController.cs index 1f122735..feb6c126 100644 --- a/src/PepperDash.Essentials.Core/Devices/IrOutputPortController.cs +++ b/src/PepperDash.Essentials.Core/Devices/IrOutputPortController.cs @@ -1,232 +1,223 @@ - - -using System; -using System.Collections.Generic; -using System.Linq; -using Crestron.SimplSharp; +using System; using Crestron.SimplSharpPro; -using Crestron.SimplSharpPro.DeviceSupport; using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core.Config; - - using PepperDash.Core; using Serilog.Events; using System.IO; using PepperDash.Core.Logging; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// IR port wrapper. May act standalone +/// +public class IrOutputPortController : Device { + uint IrPortUid; + IROutputPort IrPort; /// - /// IR port wrapper. May act standalone + /// Gets the DriverLoaded feedback /// - public class IrOutputPortController : Device - { - uint IrPortUid; - IROutputPort IrPort; + public BoolFeedback DriverLoaded { get; private set; } - /// - /// Gets the DriverLoaded feedback - /// - public BoolFeedback DriverLoaded { get; private set; } + /// + /// Gets or sets the StandardIrPulseTime + /// + public ushort StandardIrPulseTime { get; set; } - /// - /// Gets or sets the StandardIrPulseTime - /// - public ushort StandardIrPulseTime { get; set; } + /// + /// Gets or sets the DriverFilepath + /// + public string DriverFilepath { get; private set; } - /// - /// Gets or sets the DriverFilepath - /// - public string DriverFilepath { get; private set; } + /// + /// Gets or sets the DriverIsLoaded + /// + public bool DriverIsLoaded { get; private set; } - /// - /// Gets or sets the DriverIsLoaded - /// - public bool DriverIsLoaded { get; private set; } + /// + /// Gets or sets the IrFileCommands + /// + public string[] IrFileCommands { get { return IrPort.AvailableStandardIRCmds(IrPortUid); } } - /// - /// Gets or sets the IrFileCommands - /// - public string[] IrFileCommands { get { return IrPort.AvailableStandardIRCmds(IrPortUid); } } + /// + /// Gets or sets the UseBridgeJoinMap + /// + public bool UseBridgeJoinMap { get; private set; } - /// - /// Gets or sets the UseBridgeJoinMap - /// - public bool UseBridgeJoinMap { get; private set; } + /// + /// Constructor for IrDevice base class. If a null port is provided, this class will + /// still function without trying to talk to a port. + /// + public IrOutputPortController(string key, IROutputPort port, string irDriverFilepath) + : base(key) + { + //if (port == null) throw new ArgumentNullException("port"); - /// - /// Constructor for IrDevice base class. If a null port is provided, this class will - /// still function without trying to talk to a port. - /// - public IrOutputPortController(string key, IROutputPort port, string irDriverFilepath) - : base(key) + DriverLoaded = new BoolFeedback(() => DriverIsLoaded); + IrPort = port; + if (port == null) { - //if (port == null) throw new ArgumentNullException("port"); + Debug.LogMessage(LogEventLevel.Information, this, "WARNING No valid IR Port assigned to controller. IR will not function"); + return; + } + LoadDriver(irDriverFilepath); + } - DriverLoaded = new BoolFeedback(() => DriverIsLoaded); - IrPort = port; - if (port == null) + /// + /// Constructor for IrDevice base class using post activation function to get port + /// + /// key of the device + /// function to call post activation + /// config of the device + public IrOutputPortController(string key, Func postActivationFunc, + DeviceConfig config) + : base(key) + { + DriverLoaded = new BoolFeedback(() => DriverIsLoaded); + UseBridgeJoinMap = config.Properties["control"].Value("useBridgeJoinMap"); + AddPostActivationAction(() => + { + IrPort = postActivationFunc(config); + + if (IrPort == null) { Debug.LogMessage(LogEventLevel.Information, this, "WARNING No valid IR Port assigned to controller. IR will not function"); return; } - LoadDriver(irDriverFilepath); - } - /// - /// Constructor for IrDevice base class using post activation function to get port - /// - /// key of the device - /// function to call post activation - /// config of the device - public IrOutputPortController(string key, Func postActivationFunc, - DeviceConfig config) - : base(key) - { - DriverLoaded = new BoolFeedback(() => DriverIsLoaded); - UseBridgeJoinMap = config.Properties["control"].Value("useBridgeJoinMap"); - AddPostActivationAction(() => - { - IrPort = postActivationFunc(config); + // var filePath = Global.FilePathPrefix + "ir" + Global.DirectorySeparator + config.Properties["control"]["irFile"].Value(); - if (IrPort == null) - { - Debug.LogMessage(LogEventLevel.Information, this, "WARNING No valid IR Port assigned to controller. IR will not function"); - return; - } - - // var filePath = Global.FilePathPrefix + "ir" + Global.DirectorySeparator + config.Properties["control"]["irFile"].Value(); + var fileName = config.Properties["control"]["irFile"].Value(); - var fileName = config.Properties["control"]["irFile"].Value(); + var files = Directory.GetFiles(Global.FilePathPrefix, fileName, SearchOption.AllDirectories); - var files = Directory.GetFiles(Global.FilePathPrefix, fileName, SearchOption.AllDirectories); + if (files.Length == 0) + { + this.LogError("IR file {fileName} not found in {path}", fileName, Global.FilePathPrefix); + return; + } - if(files.Length == 0) - { - this.LogError("IR file {fileName} not found in {path}", fileName, Global.FilePathPrefix); - return; - } + if (files.Length > 1) + { + this.LogError("IR file {fileName} found in multiple locations: {files}", fileName, files); + return; + } - if(files.Length > 1) - { - this.LogError("IR file {fileName} found in multiple locations: {files}", fileName, files); - return; - } + var filePath = files[0]; - var filePath = files[0]; + Debug.LogMessage(LogEventLevel.Debug, "*************Attempting to load IR file: {0}***************", filePath); - Debug.LogMessage(LogEventLevel.Debug, "*************Attempting to load IR file: {0}***************", filePath); + LoadDriver(filePath); - LoadDriver(filePath); - - PrintAvailableCommands(); - }); - } + PrintAvailableCommands(); + }); + } - /// - /// PrintAvailableCommands method - /// - public void PrintAvailableCommands() - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Available IR Commands in IR File {0}", IrPortUid); - foreach (var cmd in IrPort.AvailableIRCmds()) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "{0}", cmd); - } - } - - - /// - /// Loads the IR driver at path - /// - /// path of the IR driver file - public void LoadDriver(string path) + /// + /// PrintAvailableCommands method + /// + public void PrintAvailableCommands() + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Available IR Commands in IR File {0}", IrPortUid); + foreach (var cmd in IrPort.AvailableIRCmds()) { - Debug.LogMessage(LogEventLevel.Verbose, this, "***Loading IR File***"); - if (string.IsNullOrEmpty(path)) path = DriverFilepath; - try - { - IrPortUid = IrPort.LoadIRDriver(path); - DriverFilepath = path; - StandardIrPulseTime = 200; - DriverIsLoaded = true; - - DriverLoaded.FireUpdate(); - } - catch - { - DriverIsLoaded = false; - var message = string.Format("WARNING IR Driver '{0}' failed to load", path); - Debug.LogMessage(LogEventLevel.Information, this, message); - DriverLoaded.FireUpdate(); - } + Debug.LogMessage(LogEventLevel.Verbose, this, "{0}", cmd); } + } - /// - /// PressRelease method - /// - /// IR command to send - /// true to press, false to release - /// - public virtual void PressRelease(string command, bool state) + /// + /// Loads the IR driver at path + /// + /// path of the IR driver file + public void LoadDriver(string path) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "***Loading IR File***"); + if (string.IsNullOrEmpty(path)) path = DriverFilepath; + try { - Debug.LogMessage(LogEventLevel.Verbose, this, "IR:'{0}'={1}", command, state); - if (IrPort == null) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "WARNING No IR Port assigned to controller"); - return; - } - if (!DriverIsLoaded) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "WARNING IR driver is not loaded"); - return; - } - if (state) - { - if (IrPort.IsIRCommandAvailable(IrPortUid, command)) - IrPort.Press(IrPortUid, command); - else - NoIrCommandError(command); - } - else - IrPort.Release(); + IrPortUid = IrPort.LoadIRDriver(path); + DriverFilepath = path; + StandardIrPulseTime = 200; + DriverIsLoaded = true; + + DriverLoaded.FireUpdate(); } - - /// - /// Pulse method - /// - /// IR command to send - /// time to pulse the command - /// - public virtual void Pulse(string command, ushort time) + catch + { + DriverIsLoaded = false; + var message = string.Format("WARNING IR Driver '{0}' failed to load", path); + Debug.LogMessage(LogEventLevel.Information, this, message); + DriverLoaded.FireUpdate(); + } + } + + + /// + /// PressRelease method + /// + /// IR command to send + /// true to press, false to release + /// + public virtual void PressRelease(string command, bool state) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "IR:'{0}'={1}", command, state); + if (IrPort == null) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "WARNING No IR Port assigned to controller"); + return; + } + if (!DriverIsLoaded) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "WARNING IR driver is not loaded"); + return; + } + if (state) { - if (IrPort == null) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "WARNING No IR Port assigned to controller"); - return; - } - if (!DriverIsLoaded) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "WARNING IR driver is not loaded"); - return; - } if (IrPort.IsIRCommandAvailable(IrPortUid, command)) - IrPort.PressAndRelease(IrPortUid, command, time); + IrPort.Press(IrPortUid, command); else NoIrCommandError(command); } + else + IrPort.Release(); + } - /// - /// Notifies the console when a bad command is used. - /// - /// command that was not found - protected void NoIrCommandError(string command) + /// + /// Pulse method + /// + /// IR command to send + /// time to pulse the command + /// + public virtual void Pulse(string command, ushort time) + { + if (IrPort == null) { - Debug.LogMessage(LogEventLevel.Verbose, this, "Device {0}: IR Driver {1} does not contain command {2}", - Key, IrPort.IRDriverFileNameByIRDriverId(IrPortUid), command); + Debug.LogMessage(LogEventLevel.Verbose, this, "WARNING No IR Port assigned to controller"); + return; } + if (!DriverIsLoaded) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "WARNING IR driver is not loaded"); + return; + } + if (IrPort.IsIRCommandAvailable(IrPortUid, command)) + IrPort.PressAndRelease(IrPortUid, command, time); + else + NoIrCommandError(command); + } + + /// + /// Notifies the console when a bad command is used. + /// + /// command that was not found + protected void NoIrCommandError(string command) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Device {0}: IR Driver {1} does not contain command {2}", + Key, IrPort.IRDriverFileNameByIRDriverId(IrPortUid), command); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs b/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs index 821a0c2b..04f14ac9 100644 --- a/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs @@ -1,113 +1,115 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using PepperDash.Core; -using PepperDash.Essentials.Core.Devices; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a level control item in a list, which can be used to control volume or mute functionality. +/// +public class LevelControlListItem : AudioControlListItemBase +{ + + /// + /// A reference to the IBasicVolumeWithFeedback device for control. + /// + [JsonIgnore] + public IBasicVolumeWithFeedback LevelControl + { + get + { + if (_levelControl == null) + _levelControl = DeviceManager.GetDeviceForKey(ParentDeviceKey) as IBasicVolumeWithFeedback; + return _levelControl; + } + } + IBasicVolumeWithFeedback _levelControl; + + /// + /// Gets the name from the device if it implements IKeyName or else returns the Name property + /// + [JsonProperty("preferredName")] + public string PreferredName + { + get + { + if (!string.IsNullOrEmpty(Name)) return Name; + else + { + if (LevelControl is IKeyName namedLevelControl) + { + if (namedLevelControl == null) + return "---"; + return namedLevelControl.Name; + } + else return "---"; + } + } + } + + /// + /// The key of the device in the DeviceManager for control + /// + [JsonProperty("deviceKey")] + public string DeviceKey + { + get + { + if (string.IsNullOrEmpty(ItemKey)) return ParentDeviceKey; + else + { + return DeviceManager.AllDevices. + Where(d => d.Key.Contains(ParentDeviceKey) && d.Key.Contains(ItemKey)).FirstOrDefault()?.Key ?? $"{ParentDeviceKey}--{ItemKey}"; + } + } + } + + /// + /// Indicates if the item is a level, mute , or both + /// + [JsonProperty("type")] + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public eLevelControlType Type { get; set; } + + + /// + /// Indicates if the item is a mic or not. + /// + [JsonProperty("isMic", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsMic { get; set; } + + /// + /// Indicates if the item should show the raw level in the UI. + /// + [JsonProperty("showRawLevel", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowRawLevel { get; set; } +} + + +/// +/// Indicates the type of level control item. +/// +[Flags] +public enum eLevelControlType { /// - /// Represents a level control item in a list, which can be used to control volume or mute functionality. + /// Indicates that the item is a level control only /// - public class LevelControlListItem : AudioControlListItemBase - { - - /// - /// A reference to the IBasicVolumeWithFeedback device for control. - /// - [JsonIgnore] - public IBasicVolumeWithFeedback LevelControl - { - get - { - if (_levelControl == null) - _levelControl = DeviceManager.GetDeviceForKey(ParentDeviceKey) as IBasicVolumeWithFeedback; - return _levelControl; - } - } - IBasicVolumeWithFeedback _levelControl; - - /// - /// Gets the name from the device if it implements IKeyName or else returns the Name property - /// - [JsonProperty("preferredName")] - public string PreferredName - { - get - { - if (!string.IsNullOrEmpty(Name)) return Name; - else - { - if (LevelControl is IKeyName namedLevelControl) - { - if (namedLevelControl == null) - return "---"; - return namedLevelControl.Name; - } - else return "---"; - } - } - } - - /// - /// The key of the device in the DeviceManager for control - /// - [JsonProperty("deviceKey")] - public string DeviceKey - { - get - { - if (string.IsNullOrEmpty(ItemKey)) return ParentDeviceKey; - else - { - return DeviceManager.AllDevices. - Where(d => d.Key.Contains(ParentDeviceKey) && d.Key.Contains(ItemKey)).FirstOrDefault()?.Key ?? $"{ParentDeviceKey}--{ItemKey}"; - } - } - } - - /// - /// Indicates if the item is a level, mute , or both - /// - [JsonProperty("type")] - [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - public eLevelControlType Type { get; set; } - - - /// - /// Indicates if the item is a mic or not. - /// - [JsonProperty("isMic", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsMic { get; set; } - - /// - /// Indicates if the item should show the raw level in the UI. - /// - [JsonProperty("showRawLevel", NullValueHandling = NullValueHandling.Ignore)] - public bool? ShowRawLevel { get; set; } - } - + Level = 1, /// - /// Indicates the type of level control item. + /// Indicates that the item is a mute control only /// - [Flags] - public enum eLevelControlType - { - /// - /// Indicates that the item is a level control only - /// - Level = 1, - /// - /// Indicates that the item is a mute control only - /// - Mute = 2, - /// - /// Indicates that the item is both a level and mute control - /// - LevelAndMute = Level | Mute, - } - + Mute = 2, + /// + /// Indicates that the item is both a level and mute control + /// + LevelAndMute = Level | Mute, } + + + + + + + diff --git a/src/PepperDash.Essentials.Core/Devices/PduInterfaces.cs b/src/PepperDash.Essentials.Core/Devices/PduInterfaces.cs index 1c0431b5..752ff3ac 100644 --- a/src/PepperDash.Essentials.Core/Devices/PduInterfaces.cs +++ b/src/PepperDash.Essentials.Core/Devices/PduInterfaces.cs @@ -4,33 +4,32 @@ using Crestron.SimplSharp; using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.Devices +namespace PepperDash.Essentials.Core.Devices; + +/// +/// Interface for any device that is able to control its power and has a configurable reboot time +/// +public interface IHasPowerCycle : IKeyName, IHasPowerControlWithFeedback { /// - /// Interface for any device that is able to control it'spower and has a configurable reboot time - /// - public interface IHasPowerCycle : IKeyName, IHasPowerControlWithFeedback - { - /// - /// Delay between power off and power on for reboot - /// - int PowerCycleTimeMs { get;} - - /// - /// Reboot outlet - /// - void PowerCycle(); - } + /// Delay between power off and power on for reboot + /// + int PowerCycleTimeMs { get;} /// - /// Interface for any device that contains a collection of IHasPowerReboot Devices - /// - public interface IHasControlledPowerOutlets : IKeyName - { - /// - /// Collection of IPduOutlets - /// - ReadOnlyDictionary PduOutlets { get; } + /// Reboot outlet + /// + void PowerCycle(); +} + +/// +/// Interface for any device that contains a collection of IHasPowerReboot Devices +/// +public interface IHasControlledPowerOutlets : IKeyName +{ + /// + /// Collection of IPduOutlets + /// + ReadOnlyDictionary PduOutlets { get; } - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/PowerInterfaces.cs b/src/PepperDash.Essentials.Core/Devices/PowerInterfaces.cs index ef0173d5..a4f772d3 100644 --- a/src/PepperDash.Essentials.Core/Devices/PowerInterfaces.cs +++ b/src/PepperDash.Essentials.Core/Devices/PowerInterfaces.cs @@ -1,153 +1,150 @@ using Crestron.SimplSharp; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines the contract for IHasBatteryStats +/// +public interface IHasBatteryStats : IKeyName { /// - /// Defines the contract for IHasBatteryStats + /// Gets the BatteryPercentage /// - public interface IHasBatteryStats : IKeyName - { - /// - /// Gets the BatteryPercentage - /// - int BatteryPercentage { get; } - - /// - /// Gets the BatteryCautionThresholdPercentage - /// - int BatteryCautionThresholdPercentage { get; } - - /// - /// Gets the BatteryWarningThresholdPercentage - /// - int BatteryWarningThresholdPercentage { get; } - - /// - /// Gets the BatteryIsWarningFeedback - /// - BoolFeedback BatteryIsWarningFeedback { get; } - - /// - /// Gets the BatteryIsCautionFeedback - /// - BoolFeedback BatteryIsCautionFeedback { get; } - - /// - /// Gets the BatteryIsOkFeedback - /// - BoolFeedback BatteryIsOkFeedback { get; } - - /// - /// Gets the BatteryPercentageFeedback - /// - IntFeedback BatteryPercentageFeedback { get; } - } + int BatteryPercentage { get; } /// - /// Defines the contract for IHasBatteryCharging + /// Gets the BatteryCautionThresholdPercentage /// - public interface IHasBatteryCharging : IHasBatteryStats - { - /// - /// Gets the BatteryIsCharging - /// - BoolFeedback BatteryIsCharging { get; } - } + int BatteryCautionThresholdPercentage { get; } /// - /// Interface for any device that has multiple batteries that can be monitored + /// Gets the BatteryWarningThresholdPercentage /// - public interface IHasBatteries : IKeyName - { - /// - /// Collection of batteries - /// - ReadOnlyDictionary Batteries { get; } - } + int BatteryWarningThresholdPercentage { get; } /// - /// Defines the contract for IHasBatteryStatsExtended + /// Gets the BatteryIsWarningFeedback /// - public interface IHasBatteryStatsExtended : IHasBatteryStats - { - /// - /// Gets the InputVoltage in millivolts - /// - int InputVoltage { get; } - - /// - /// Gets the OutputVoltage in millivolts - /// - int OutputVoltage { get; } - - /// - /// Gets the InputCurrent in milliamps - /// - int InptuCurrent { get; } - - /// - /// Gets the OutputCurrent in milliamps - /// - int OutputCurrent { get; } - - /// - /// Gets the InputVoltageFeedback - /// - IntFeedback InputVoltageFeedback { get; } - - /// - /// Gets the OutputVoltageFeedback - /// - IntFeedback OutputVoltageFeedback { get; } - - /// - /// Gets the InputCurrentFeedback - /// - IntFeedback InputCurrentFeedback { get; } - - /// - /// Gets the OutputCurrentFeedback - /// - IntFeedback OutputCurrentFeedback { get; } - } + BoolFeedback BatteryIsWarningFeedback { get; } /// - /// Defines the contract for IHasPowerCycleWithBattery + /// Gets the BatteryIsCautionFeedback /// - public interface IHasPowerCycleWithBattery : IHasPowerCycle, IHasBatteryStats - { - - } + BoolFeedback BatteryIsCautionFeedback { get; } /// - /// Interface for any device that is able to control it's power and has a configurable reboot time + /// Gets the BatteryIsOkFeedback /// - public interface IHasPowerCycle : IKeyName, IHasPowerControlWithFeedback - { - /// - /// Delay between power off and power on for reboot - /// - int PowerCycleTimeMs { get; } - - /// - /// Reboot outlet - /// - void PowerCycle(); - } + BoolFeedback BatteryIsOkFeedback { get; } /// - /// Interface for any device that contains a collection of IHasPowerReboot Devices + /// Gets the BatteryPercentageFeedback /// - public interface IHasControlledPowerOutlets : IKeyName - { - /// - /// Collection of IPduOutlets - /// - ReadOnlyDictionary PduOutlets { get; } + IntFeedback BatteryPercentageFeedback { get; } +} - } +/// +/// Defines the contract for IHasBatteryCharging +/// +public interface IHasBatteryCharging : IHasBatteryStats +{ + /// + /// Gets the BatteryIsCharging + /// + BoolFeedback BatteryIsCharging { get; } +} +/// +/// Interface for any device that has multiple batteries that can be monitored +/// +public interface IHasBatteries : IKeyName +{ + /// + /// Collection of batteries + /// + ReadOnlyDictionary Batteries { get; } +} +/// +/// Defines the contract for IHasBatteryStatsExtended +/// +public interface IHasBatteryStatsExtended : IHasBatteryStats +{ + /// + /// Gets the InputVoltage in millivolts + /// + int InputVoltage { get; } -} \ No newline at end of file + /// + /// Gets the OutputVoltage in millivolts + /// + int OutputVoltage { get; } + + /// + /// Gets the InputCurrent in milliamps + /// + int InptuCurrent { get; } + + /// + /// Gets the OutputCurrent in milliamps + /// + int OutputCurrent { get; } + + /// + /// Gets the InputVoltageFeedback + /// + IntFeedback InputVoltageFeedback { get; } + + /// + /// Gets the OutputVoltageFeedback + /// + IntFeedback OutputVoltageFeedback { get; } + + /// + /// Gets the InputCurrentFeedback + /// + IntFeedback InputCurrentFeedback { get; } + + /// + /// Gets the OutputCurrentFeedback + /// + IntFeedback OutputCurrentFeedback { get; } +} + +/// +/// Interface for any device that is able to control its power, has a configurable reboot time, and has batteries that can be monitored +/// +public interface IHasPowerCycleWithBattery : IHasPowerCycle, IHasBatteryStats +{ + +} + +/// +/// Interface for any device that is able to control it's power and has a configurable reboot time +/// +public interface IHasPowerCycle : IKeyName, IHasPowerControlWithFeedback +{ + + /// + /// Delay between power off and power on for reboot + /// + int PowerCycleTimeMs { get; } + + /// + /// Reboot outlet + /// + void PowerCycle(); +} + +/// +/// Interface for any device that contains a collection of IHasPowerReboot Devices +/// +public interface IHasControlledPowerOutlets : IKeyName +{ + /// + /// Collection of PDU outlets + /// + ReadOnlyDictionary PduOutlets { get; } + +} diff --git a/src/PepperDash.Essentials.Core/Devices/PresentationDeviceType.cs b/src/PepperDash.Essentials.Core/Devices/PresentationDeviceType.cs index 44474832..ed51738a 100644 --- a/src/PepperDash.Essentials.Core/Devices/PresentationDeviceType.cs +++ b/src/PepperDash.Essentials.Core/Devices/PresentationDeviceType.cs @@ -1,13 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro; -using Crestron.SimplSharpPro.DeviceSupport; -using Crestron.SimplSharpPro.EthernetCommunication; -using Crestron.SimplSharpPro.UI; - -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core { /// /// Enumeration of PresentationSourceType values @@ -44,4 +35,4 @@ namespace PepperDash.Essentials.Core /// VCR } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Core/Devices/PresetListItem.cs b/src/PepperDash.Essentials.Core/Devices/PresetListItem.cs index 625dcec9..eb9fd026 100644 --- a/src/PepperDash.Essentials.Core/Devices/PresetListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/PresetListItem.cs @@ -1,51 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json; +using Newtonsoft.Json; using PepperDash.Core; +namespace PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core +/// +/// Represents a PresetListItem +/// + +public class PresetListItem : AudioControlListItemBase { /// - /// Represents a PresetListItem + /// Gets the preset associated with this list item /// - public class PresetListItem : AudioControlListItemBase + [JsonIgnore] + public IKeyName Preset + { - /// - /// Gets the preset associated with this list item - /// - [JsonIgnore] - public IKeyName Preset + get { - get + if (_preset == null) { - if (_preset == null) - { - var parent = DeviceManager.GetDeviceForKey(ParentDeviceKey) as IDspPresets; - if (parent == null || !parent.Presets.ContainsKey(ItemKey)) - return null; - _preset = parent.Presets[ItemKey]; - } - return _preset; + var parent = DeviceManager.GetDeviceForKey(ParentDeviceKey) as IDspPresets; + if (parent == null || !parent.Presets.ContainsKey(ItemKey)) + return null; + _preset = parent.Presets[ItemKey]; } + return _preset; } - private IKeyName _preset; + } + private IKeyName _preset; - /// - /// Gets the name from the device if it implements IKeyName or else returns the Name property - /// - [JsonProperty("preferredName")] - public string PreferredName + /// + /// Gets the name from the device if it implements IKeyName or else returns the Name property + /// + [JsonProperty("preferredName")] + public string PreferredName + { + get { - get - { - if (!string.IsNullOrEmpty(Name)) return Name; + if (!string.IsNullOrEmpty(Name)) return Name; - else return Preset.Name; - } + else return Preset.Name; } } } diff --git a/src/PepperDash.Essentials.Core/Devices/ReconfigurableDevice.cs b/src/PepperDash.Essentials.Core/Devices/ReconfigurableDevice.cs index ccb74a39..a7dc2af2 100644 --- a/src/PepperDash.Essentials.Core/Devices/ReconfigurableDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/ReconfigurableDevice.cs @@ -12,95 +12,67 @@ using PepperDash.Essentials.Core.Config; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace PepperDash.Essentials.Core.Devices +namespace PepperDash.Essentials.Core.Devices; + +/// +/// +/// +public abstract class ReconfigurableDevice : EssentialsDevice, IReconfigurableDevice { - /// - /// - /// - public abstract class ReconfigurableDevice : EssentialsDevice, IReconfigurableDevice + public event EventHandler ConfigChanged; + + public DeviceConfig Config { get; private set; } + + protected ReconfigurableDevice(DeviceConfig config) + : base(config.Key) { - /// - /// Event fired when the configuration changes - /// - public event EventHandler ConfigChanged; + SetNameHelper(config); - /// - /// Gets the current DeviceConfig - /// - public DeviceConfig Config { get; private set; } - - /// - /// Constructor - /// - /// config of the device - protected ReconfigurableDevice(DeviceConfig config) - : base(config.Key) - { - SetNameHelper(config); - - Config = config; - } - - /// - /// Sets the Config, calls CustomSetConfig and fires the ConfigChanged event - /// - /// - /// - /// SetConfig method - /// - public void SetConfig(DeviceConfig config) - { - Config = config; - - SetNameHelper(config); - - CustomSetConfig(config); - - var handler = ConfigChanged; - if (handler != null) - { - handler(this, new EventArgs()); - } - } - - void SetNameHelper(DeviceConfig config) - { - if (!string.IsNullOrEmpty(config.Name)) - Name = config.Name; - } - - - - /// - /// Used by the extending class to allow for any custom actions to be taken (tell the ConfigWriter to write config, etc) - /// - /// config of the device - protected virtual void CustomSetConfig(DeviceConfig config) - { - ConfigWriter.UpdateDeviceConfig(config); - } + Config = config; } /// - /// A ReconfigurableDevice that is also bridgeable + /// Sets the Config, calls CustomSetConfig and fires the ConfigChanged event /// - public abstract class ReconfigurableBridgableDevice : ReconfigurableDevice, IBridgeAdvanced + /// + public void SetConfig(DeviceConfig config) { - /// - /// Constructor - /// - /// config of the device - protected ReconfigurableBridgableDevice(DeviceConfig config) : base(config) - { - } + Config = config; - /// - /// LinkToApi method - /// - /// trilist to link - /// the join to start at - /// key to the join map - /// the bridge to use - public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); + SetNameHelper(config); + + CustomSetConfig(config); + + var handler = ConfigChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } } + + void SetNameHelper(DeviceConfig config) + { + if (!string.IsNullOrEmpty(config.Name)) + Name = config.Name; + } + + + + /// + /// Used by the extending class to allow for any custom actions to be taken (tell the ConfigWriter to write config, etc) + /// + /// + protected virtual void CustomSetConfig(DeviceConfig config) + { + ConfigWriter.UpdateDeviceConfig(config); + } +} + +public abstract class ReconfigurableBridgableDevice : ReconfigurableDevice, IBridgeAdvanced +{ + protected ReconfigurableBridgableDevice(DeviceConfig config) : base(config) + { + } + + public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/SmartObjectBaseTypes.cs b/src/PepperDash.Essentials.Core/Devices/SmartObjectBaseTypes.cs index 55e70de4..1b13203a 100644 --- a/src/PepperDash.Essentials.Core/Devices/SmartObjectBaseTypes.cs +++ b/src/PepperDash.Essentials.Core/Devices/SmartObjectBaseTypes.cs @@ -1,24 +1,23 @@  -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a SmartObjectJoinOffsets +/// +public class SmartObjectJoinOffsets { - /// - /// Represents a SmartObjectJoinOffsets - /// - public class SmartObjectJoinOffsets - { - /// - /// Dpad Join Offset - /// - public const ushort Dpad = 1; + /// + /// Dpad Join Offset + /// + public const ushort Dpad = 1; - /// - /// Numpad Join Offset - /// - public const ushort Numpad = 2; + /// + /// Numpad Join Offset + /// + public const ushort Numpad = 2; - /// - /// PresetList Join Offset - /// - public const ushort PresetList = 6; - } + /// + /// PresetList Join Offset + /// + public const ushort PresetList = 6; } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs b/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs index deaeeae3..7ffd711d 100644 --- a/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs @@ -4,344 +4,252 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines the type of source list item, which can be a route, off, or other. +/// +public enum eSourceListItemType { + /// + /// Represents a typical route. + /// + Route, + /// + /// Represents an off route. + /// + Off, + /// + /// Represents some other type of route + /// + Other, +} + +/// +/// Represents an item in a source list - can be deserialized into. +/// +public class SourceListItem +{ + /// + /// The key of the source item, which is used to identify it in the DeviceManager + /// + [JsonProperty("sourceKey")] + public string SourceKey { get; set; } + /// /// Defines the type of source list item, which can be a route, off, or other. /// This is used to categorize the source list items in a room. /// The type is serialized to JSON and can be used to determine how the item should be displayed or handled in the UI. + /// Returns the source Device for this, if it exists in DeviceManager /// - public enum eSourceListItemType + [JsonIgnore] + public Device SourceDevice { - /// - /// Represents a typical route. - /// - Route, - /// - /// Represents an off route. - /// - Off, - /// - /// Represents some other type of route - /// - Other, + get + { + if (_SourceDevice == null) + _SourceDevice = DeviceManager.GetDeviceForKey(SourceKey) as Device; + return _SourceDevice; + } } - /// - /// Represents a SourceListItem - /// - public class SourceListItem - { - /// - /// The key of the source item, which is used to identify it in the DeviceManager - /// - [JsonProperty("sourceKey")] - public string SourceKey { get; set; } + private Device _SourceDevice; - /// - /// Returns the source Device for this, if it exists in DeviceManager - /// - [JsonIgnore] - public Device SourceDevice + /// + /// Gets either the source's Name or this AlternateName property, if + /// defined. If source doesn't exist, returns "Missing source" + /// + [JsonProperty("preferredName")] + public string PreferredName + { + get { - get + if (string.IsNullOrEmpty(Name)) { - if (_SourceDevice == null) - _SourceDevice = DeviceManager.GetDeviceForKey(SourceKey) as Device; - return _SourceDevice; + if (SourceDevice == null) + return "---"; + return SourceDevice.Name; } - } - - private Device _SourceDevice; - - /// - /// Gets either the source's Name or this AlternateName property, if - /// defined. If source doesn't exist, returns "Missing source" - /// - [JsonProperty("preferredName")] - public string PreferredName - { - get - { - if (string.IsNullOrEmpty(Name)) - { - if (SourceDevice == null) - return "---"; - return SourceDevice.Name; - } - return Name; - } - } - - /// - /// A name that will override the source's name on the UI - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// Specifies and icon for the source list item - /// - [JsonProperty("icon")] - public string Icon { get; set; } - - /// - /// Alternate icon - /// - [JsonProperty("altIcon")] - public string AltIcon { get; set; } - - /// - /// Indicates if the item should be included in the source list - /// - [JsonProperty("includeInSourceList")] - public bool IncludeInSourceList { get; set; } - - /// - /// Used to specify the order of the items in the source list when displayed - /// - [JsonProperty("order")] - public int Order { get; set; } - - /// - /// The key of the device for volume control - /// - [JsonProperty("volumeControlKey")] - public string VolumeControlKey { get; set; } - - /// - /// The type of source list item - /// - [JsonProperty("type")] - [JsonConverter(typeof(StringEnumConverter))] - public eSourceListItemType Type { get; set; } - - /// - /// The list of routes to execute for this source list item - /// - [JsonProperty("routeList")] - public List RouteList { get; set; } - - /// - /// Indicates if this source should be disabled for sharing to the far end call participants via codec content - /// - [JsonProperty("disableCodecSharing")] - public bool DisableCodecSharing { get; set; } - - /// - /// Indicates if this source should be disabled for routing to a shared output - /// - [JsonProperty("disableRoutedSharing")] - public bool DisableRoutedSharing { get; set; } - - /// - /// - /// - [JsonProperty("destinations")] - public List Destinations { get; set; } - /// - /// A means to reference a source list for this source item, in the event that this source has an input that can have sources routed to it - /// - [JsonProperty("sourceListKey")] - public string SourceListKey { get; set; } - - /// - /// Indicates if the device associated with this source is controllable - /// - [JsonProperty("isControllable")] - public bool IsControllable { get; set; } - - /// - /// Indicates that the device associated with this source has audio available - /// - [JsonProperty("isAudioSource")] - public bool IsAudioSource { get; set; } - - /// - /// Hide source on UI when Avanced Sharing is enabled - /// - [JsonProperty("disableAdvancedRouting")] - public bool DisableAdvancedRouting { get; set; } - - /// - /// Hide source on UI when Simpl Sharing is enabled - /// - [JsonProperty("disableSimpleRouting")] - public bool DisableSimpleRouting { get; set; } - - /// - /// The key of the device that provides video sync for this source item - /// - [JsonProperty("syncProviderDeviceKey")] - public string SyncProviderDeviceKey { get; set; } - - /// - /// Indicates if the source supports USB connections - /// - [JsonProperty("supportsUsb")] - public bool SupportsUsb { get; set; } - - /// - /// The key of the source port associated with this source item - /// This is used to identify the specific port on the source device that this item refers to for advanced routing - /// - [JsonProperty("sourcePortKey")] - public string SourcePortKey { get; set; } - - - /// - /// Default constructor for SourceListItem, initializes the Icon to "Blank" - /// - public SourceListItem() - { - Icon = "Blank"; - } - - /// - /// Returns a string representation of the SourceListItem, including the SourceKey and Name - /// - /// A string representation of the SourceListItem - public override string ToString() - { - return $"{SourceKey}:{Name}"; + return Name; } } /// - /// Represents a route in a source list item, which defines the source and destination keys and the type of signal being routed + /// A name that will override the source's name on the UI /// - public class SourceRouteListItem + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Specifies and icon for the source list item + /// + [JsonProperty("icon")] + public string Icon { get; set; } + + /// + /// Alternate icon + /// + [JsonProperty("altIcon")] + public string AltIcon { get; set; } + + /// + /// Indicates if the item should be included in the source list + /// + [JsonProperty("includeInSourceList")] + public bool IncludeInSourceList { get; set; } + + /// + /// Used to specify the order of the items in the source list when displayed + /// + [JsonProperty("order")] + public int Order { get; set; } + + /// + /// The key of the device for volume control + /// + [JsonProperty("volumeControlKey")] + public string VolumeControlKey { get; set; } + + /// + /// The type of source list item + /// + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter))] + public eSourceListItemType Type { get; set; } + + /// + /// The list of routes to execute for this source list item + /// + [JsonProperty("routeList")] + public List RouteList { get; set; } + + /// + /// Indicates if this source should be disabled for sharing to the far end call participants via codec content + /// + [JsonProperty("disableCodecSharing")] + public bool DisableCodecSharing { get; set; } + + /// + /// Indicates if this source should be disabled for routing to a shared output + /// + [JsonProperty("disableRoutedSharing")] + public bool DisableRoutedSharing { get; set; } + + /// + /// A means to reference a source list for this source item, in the event that this source has an input that can have sources routed to it + /// + [JsonProperty("sourceListKey")] + public string SourceListKey { get; set; } + + /// + /// Indicates if the device associated with this source is controllable + /// + [JsonProperty("isControllable")] + public bool IsControllable { get; set; } + + /// + /// Indicates that the device associated with this source has audio available + /// + [JsonProperty("isAudioSource")] + public bool IsAudioSource { get; set; } + + /// + /// Hide source on UI when Avanced Sharing is enabled + /// + [JsonProperty("disableAdvancedRouting")] + public bool DisableAdvancedRouting { get; set; } + + /// + /// Hide source on UI when Simpl Sharing is enabled + /// + [JsonProperty("disableSimpleRouting")] + public bool DisableSimpleRouting { get; set; } + + /// + /// The key of the device that provides video sync for this source item + /// + [JsonProperty("syncProviderDeviceKey")] + public string SyncProviderDeviceKey { get; set; } + + /// + /// Indicates if the source supports USB connections + /// + [JsonProperty("supportsUsb")] + public bool SupportsUsb { get; set; } + + /// + /// The key of the source port associated with this source item + /// This is used to identify the specific port on the source device that this item refers to for advanced routing + /// + [JsonProperty("sourcePortKey")] + public string SourcePortKey { get; set; } + + + /// + /// Default constructor for SourceListItem, initializes the Icon to "Blank" + /// + public SourceListItem() { - /// - /// The key of the source device to route from - /// - [JsonProperty("sourceKey")] - public string SourceKey { get; set; } - - /// - /// The key of the source port to route from - /// - [JsonProperty("sourcePortKey")] - public string SourcePortKey { get; set; } - - /// - /// The key of the destination device to route to - /// - [JsonProperty("destinationKey")] - public string DestinationKey { get; set; } - - /// - /// The key of the destination port to route to - /// - [JsonProperty("destinationPortKey")] - public string DestinationPortKey { get; set; } - - /// - /// The type of signal being routed, such as audio or video - /// - [JsonProperty("type")] - public eRoutingSignalType Type { get; set; } - - /// - /// Key for a destination list item. If BOTH SourceListItemKey AND DestinationListItemKey are defined, - /// then the direct route method should be used. - /// - [JsonProperty("destinationListItemKey", NullValueHandling = NullValueHandling.Ignore)] - public string DestinationListItemKey { get; set; } - - /// - /// Key for a source list item. If BOTH SourceListItemKey AND DestinationListItemKey are defined, - /// then the direct route method should be used. - /// - [JsonProperty("sourceListItemKey", NullValueHandling = NullValueHandling.Ignore)] - public string SourceListItemKey { get; set; } + Icon = "Blank"; } /// - /// Defines the valid destination types for SourceListItems in a room + /// Returns a string representation of the SourceListItem, including the SourceKey and Name /// - [Obsolete] - public enum eSourceListItemDestinationTypes + /// A string representation of the SourceListItem + public override string ToString() { - /// - /// Default display, used for the main video output in a room - /// - defaultDisplay, - /// - /// Left display - /// - leftDisplay, - /// - /// Right display - /// - rightDisplay, - /// - /// Center display - /// - centerDisplay, - /// - /// Program audio, used for the main audio output in a room - /// - programAudio, - /// - /// Codec content, used for sharing content to the far end in a video call - /// - codecContent, - /// - /// Front left display, used for rooms with multiple displays - /// - frontLeftDisplay, - /// - /// Front right display, used for rooms with multiple displays - /// - frontRightDisplay, - /// - /// Rear left display, used for rooms with multiple displays - /// - rearLeftDisplay, - /// - /// Rear right display, used for rooms with multiple displays - /// - rearRightDisplay, - /// - /// Auxiliary display 1, used for additional displays in a room - /// - auxDisplay1, - /// - /// Auxiliary display 2, used for additional displays in a room - /// - auxDisplay2, - /// - /// Auxiliary display 3, used for additional displays in a room - /// - auxDisplay3, - /// - /// Auxiliary display 4, used for additional displays in a room - /// - auxDisplay4, - /// - /// Auxiliary display 5, used for additional displays in a room - /// - auxDisplay5, - /// - /// Auxiliary display 6, used for additional displays in a room - /// - auxDisplay6, - /// - /// Auxiliary display 7, used for additional displays in a room - /// - auxDisplay7, - /// - /// Auxiliary display 8, used for additional displays in a room - /// - auxDisplay8, - /// - /// Auxiliary display 9, used for additional displays in a room - /// - auxDisplay9, - /// - /// Auxiliary display 10, used for additional displays in a room - /// - auxDisplay10, + return $"{SourceKey}:{Name}"; } -} \ No newline at end of file +} + +/// +/// Represents a route in a source list item, which defines the source and destination keys and the type of signal being routed +/// +public class SourceRouteListItem +{ + /// + /// The key of the source device to route from + /// + [JsonProperty("sourceKey")] + public string SourceKey { get; set; } + + /// + /// The key of the source port to route from + /// + [JsonProperty("sourcePortKey")] + public string SourcePortKey { get; set; } + + /// + /// The key of the destination device to route to + /// + [JsonProperty("destinationKey")] + public string DestinationKey { get; set; } + + /// + /// The key of the destination port to route to + /// + [JsonProperty("destinationPortKey")] + public string DestinationPortKey { get; set; } + + /// + /// The type of signal being routed, such as audio or video + /// + [JsonProperty("type")] + public eRoutingSignalType Type { get; set; } + + /// + /// Key for a destination list item. If BOTH SourceListItemKey AND DestinationListItemKey are defined, + /// then the direct route method should be used. + /// + [JsonProperty("destinationListItemKey", NullValueHandling = NullValueHandling.Ignore)] + public string DestinationListItemKey { get; set; } + + /// + /// Key for a source list item. If BOTH SourceListItemKey AND DestinationListItemKey are defined, + /// then the direct route method should be used. + /// + [JsonProperty("sourceListItemKey", NullValueHandling = NullValueHandling.Ignore)] + public string SourceListItemKey { get; set; } +} + diff --git a/src/PepperDash.Essentials.Core/Devices/VolumeDeviceChangeEventArgs.cs b/src/PepperDash.Essentials.Core/Devices/VolumeDeviceChangeEventArgs.cs index ae2895db..7aeac9ee 100644 --- a/src/PepperDash.Essentials.Core/Devices/VolumeDeviceChangeEventArgs.cs +++ b/src/PepperDash.Essentials.Core/Devices/VolumeDeviceChangeEventArgs.cs @@ -1,14 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using PepperDash.Core; -using PepperDash.Essentials.Core; +namespace PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core -{ /// /// /// @@ -58,4 +51,3 @@ namespace PepperDash.Essentials.Core /// DidChange } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/eSourceListItemDestinationTypes.cs b/src/PepperDash.Essentials.Core/Devices/eSourceListItemDestinationTypes.cs new file mode 100644 index 00000000..7b9e9b35 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/eSourceListItemDestinationTypes.cs @@ -0,0 +1,94 @@ +using System; + +namespace PepperDash.Essentials.Core; + +/// +/// Defines the eSourceListItemDestinationTypes enumeration, which represents the various destination types for source list items in a room control system. +/// This enumeration is marked as obsolete, indicating that it may be removed in future versions and should not be used in new development. +/// Each member of the enumeration corresponds to a specific type of display or audio output commonly found in room control systems, +/// such as default displays, program audio, codec content, and auxiliary displays. +/// +[Obsolete] +public enum eSourceListItemDestinationTypes +{ + /// + /// Default display, used for the main video output in a room + /// + defaultDisplay, + /// + /// Left display + /// + leftDisplay, + /// + /// Right display + /// + rightDisplay, + /// + /// Center display + /// + centerDisplay, + /// + /// Program audio, used for the main audio output in a room + /// + programAudio, + /// + /// Codec content, used for sharing content to the far end in a video call + /// + codecContent, + /// + /// Front left display, used for rooms with multiple displays + /// + frontLeftDisplay, + /// + /// Front right display, used for rooms with multiple displays + /// + frontRightDisplay, + /// + /// Rear left display, used for rooms with multiple displays + /// + rearLeftDisplay, + /// + /// Rear right display, used for rooms with multiple displays + /// + rearRightDisplay, + /// + /// Auxiliary display 1, used for additional displays in a room + /// + auxDisplay1, + /// + /// Auxiliary display 2, used for additional displays in a room + /// + auxDisplay2, + /// + /// Auxiliary display 3, used for additional displays in a room + /// + auxDisplay3, + /// + /// Auxiliary display 4, used for additional displays in a room + /// + auxDisplay4, + /// + /// Auxiliary display 5, used for additional displays in a room + /// + auxDisplay5, + /// + /// Auxiliary display 6, used for additional displays in a room + /// + auxDisplay6, + /// + /// Auxiliary display 7, used for additional displays in a room + /// + auxDisplay7, + /// + /// Auxiliary display 8, used for additional displays in a room + /// + auxDisplay8, + /// + /// Auxiliary display 9, used for additional displays in a room + /// + auxDisplay9, + /// + /// Auxiliary display 10, used for additional displays in a room + /// + auxDisplay10, +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Ethernet/EthernetStatistics.cs b/src/PepperDash.Essentials.Core/Ethernet/EthernetStatistics.cs index 0f5c2863..c5097d07 100644 --- a/src/PepperDash.Essentials.Core/Ethernet/EthernetStatistics.cs +++ b/src/PepperDash.Essentials.Core/Ethernet/EthernetStatistics.cs @@ -6,52 +6,51 @@ using Crestron.SimplSharp; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.Ethernet +namespace PepperDash.Essentials.Core.Ethernet; + +/// +/// Ethernet settings feedbacks +/// +public static class EthernetSettings { /// - /// Ethernet settings feedbacks + /// Link active feedback /// - public static class EthernetSettings - { - /// - /// Link active feedback - /// - public static readonly BoolFeedback LinkActive = new BoolFeedback("LinkActive", - () => true); + public static readonly BoolFeedback LinkActive = new BoolFeedback("LinkActive", + () => true); - /// - /// DHCP active feedback - /// - public static readonly BoolFeedback DhcpActive = new BoolFeedback("DhcpActive", - () => CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, 0) == "ON"); + /// + /// DHCP active feedback + /// + public static readonly BoolFeedback DhcpActive = new BoolFeedback("DhcpActive", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, 0) == "ON"); - /// - /// Hostname feedback - /// - public static readonly StringFeedback Hostname = new StringFeedback("Hostname", - () => CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0)); + /// + /// Hostname feedback + /// + public static readonly StringFeedback Hostname = new StringFeedback("Hostname", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0)); - /// - /// IP Address feedback - /// - public static readonly StringFeedback IpAddress0 = new StringFeedback("IpAddress0", - () => CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)); + /// + /// IP Address feedback + /// + public static readonly StringFeedback IpAddress0 = new StringFeedback("IpAddress0", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)); - /// - /// Subnet Mask feedback - /// - public static readonly StringFeedback SubnetMask0 = new StringFeedback("SubnetMask0", - () => CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, 0)); + /// + /// Subnet Mask feedback + /// + public static readonly StringFeedback SubnetMask0 = new StringFeedback("SubnetMask0", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, 0)); - /// - /// Default Gateway feedback - /// - public static readonly StringFeedback DefaultGateway0 = new StringFeedback("DefaultGateway0", - () => CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, 0)); - } + /// + /// Default Gateway feedback + /// + public static readonly StringFeedback DefaultGateway0 = new StringFeedback("DefaultGateway0", + () => CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, 0)); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Extensions/IpAddressExtensions.cs b/src/PepperDash.Essentials.Core/Extensions/IpAddressExtensions.cs index 35ec1b8a..6da2211a 100644 --- a/src/PepperDash.Essentials.Core/Extensions/IpAddressExtensions.cs +++ b/src/PepperDash.Essentials.Core/Extensions/IpAddressExtensions.cs @@ -5,82 +5,81 @@ using System.Net; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Extensions for IPAddress to provide additional functionality such as getting broadcast address, network address, and checking if two addresses are in the same subnet. +/// +public static class IPAddressExtensions { /// - /// Extensions for IPAddress to provide additional functionality such as getting broadcast address, network address, and checking if two addresses are in the same subnet. + /// Get the broadcast address for a given IP address and subnet mask. /// - public static class IPAddressExtensions + /// Address to check + /// Subnet mask in a.b.c.d format + /// Broadcast address + /// + /// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the broadcast address will be 192.168.1.255 + /// + /// + public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask) { - /// - /// Get the broadcast address for a given IP address and subnet mask. - /// - /// Address to check - /// Subnet mask in a.b.c.d format - /// Broadcast address - /// - /// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the broadcast address will be 192.168.1.255 - /// - /// - public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask) + byte[] ipAdressBytes = address.GetAddressBytes(); + byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); + + if (ipAdressBytes.Length != subnetMaskBytes.Length) + throw new ArgumentException("Lengths of IP address and subnet mask do not match."); + + byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + for (int i = 0; i < broadcastAddress.Length; i++) { - byte[] ipAdressBytes = address.GetAddressBytes(); - byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); - - if (ipAdressBytes.Length != subnetMaskBytes.Length) - throw new ArgumentException("Lengths of IP address and subnet mask do not match."); - - byte[] broadcastAddress = new byte[ipAdressBytes.Length]; - for (int i = 0; i < broadcastAddress.Length; i++) - { - broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255)); - } - return new IPAddress(broadcastAddress); + broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255)); } + return new IPAddress(broadcastAddress); + } - /// - /// Get the network address for a given IP address and subnet mask. - /// - /// Address to check - /// Subnet mask in a.b.c.d - /// Network Address - /// /// - /// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the network address will be 192.168.1.0 - /// - /// - public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask) + /// + /// Get the network address for a given IP address and subnet mask. + /// + /// Address to check + /// Subnet mask in a.b.c.d + /// Network Address + /// /// + /// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the network address will be 192.168.1.0 + /// + /// + public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask) + { + byte[] ipAdressBytes = address.GetAddressBytes(); + byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); + + if (ipAdressBytes.Length != subnetMaskBytes.Length) + throw new ArgumentException("Lengths of IP address and subnet mask do not match."); + + byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + for (int i = 0; i < broadcastAddress.Length; i++) { - byte[] ipAdressBytes = address.GetAddressBytes(); - byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); - - if (ipAdressBytes.Length != subnetMaskBytes.Length) - throw new ArgumentException("Lengths of IP address and subnet mask do not match."); - - byte[] broadcastAddress = new byte[ipAdressBytes.Length]; - for (int i = 0; i < broadcastAddress.Length; i++) - { - broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i])); - } - return new IPAddress(broadcastAddress); + broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i])); } + return new IPAddress(broadcastAddress); + } - /// - /// Determine if two IP addresses are in the same subnet. - /// - /// Address to check - /// Second address to check - /// Subnet mask to use to compare the 2 IP Address - /// True if addresses are in the same subnet - /// - /// If the input IP addresses are 192.168.1.100 and 192.168.1.200, and the subnet mask is 255.255.255.0, this will return true. - /// If the input IP addresses are 10.1.1.100 and 192.168.1.100, and the subnet mask is 255.255.255.0, this will return false. - /// - public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask) - { - IPAddress network1 = address.GetNetworkAddress(subnetMask); - IPAddress network2 = address2.GetNetworkAddress(subnetMask); + /// + /// Determine if two IP addresses are in the same subnet. + /// + /// Address to check + /// Second address to check + /// Subnet mask to use to compare the 2 IP Address + /// True if addresses are in the same subnet + /// + /// If the input IP addresses are 192.168.1.100 and 192.168.1.200, and the subnet mask is 255.255.255.0, this will return true. + /// If the input IP addresses are 10.1.1.100 and 192.168.1.100, and the subnet mask is 255.255.255.0, this will return false. + /// + public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask) + { + IPAddress network1 = address.GetNetworkAddress(subnetMask); + IPAddress network2 = address2.GetNetworkAddress(subnetMask); - return network1.Equals(network2); - } + return network1.Equals(network2); } } diff --git a/src/PepperDash.Essentials.Core/Extensions/JsonExtensions.cs b/src/PepperDash.Essentials.Core/Extensions/JsonExtensions.cs index 74697525..1a6bbb6b 100644 --- a/src/PepperDash.Essentials.Core/Extensions/JsonExtensions.cs +++ b/src/PepperDash.Essentials.Core/Extensions/JsonExtensions.cs @@ -1,52 +1,44 @@ - - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Newtonsoft.Json; +using System.Collections.Generic; using Newtonsoft.Json.Linq; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// JsonExtensions class +/// +public static class JsonExtensions { /// - /// JsonExtensions class + /// FindTokens method /// - public static class JsonExtensions + /// token for the container + /// name of the token to find + /// list of matching tokens + public static List FindTokens(this JToken containerToken, string name) { - /// - /// FindTokens method - /// - /// token for the container - /// name of the token to find - /// list of matching tokens - public static List FindTokens(this JToken containerToken, string name) - { - List matches = new List(); - FindTokens(containerToken, name, matches); - return matches; - } + List matches = new List(); + FindTokens(containerToken, name, matches); + return matches; + } - private static void FindTokens(JToken containerToken, string name, List matches) + private static void FindTokens(JToken containerToken, string name, List matches) + { + if (containerToken.Type == JTokenType.Object) { - if (containerToken.Type == JTokenType.Object) + foreach (JProperty child in containerToken.Children()) { - foreach (JProperty child in containerToken.Children()) + if (child.Name == name) { - if (child.Name == name) - { - matches.Add(child.Value); - } - FindTokens(child.Value, name, matches); + matches.Add(child.Value); } + FindTokens(child.Value, name, matches); } - else if (containerToken.Type == JTokenType.Array) + } + else if (containerToken.Type == JTokenType.Array) + { + foreach (JToken child in containerToken.Children()) { - foreach (JToken child in containerToken.Children()) - { - FindTokens(child, name, matches); - } + FindTokens(child, name, matches); } } } diff --git a/src/PepperDash.Essentials.Core/Extensions/StringExtensions.cs b/src/PepperDash.Essentials.Core/Extensions/StringExtensions.cs index 9dbb6244..bdbe69ad 100644 --- a/src/PepperDash.Essentials.Core/Extensions/StringExtensions.cs +++ b/src/PepperDash.Essentials.Core/Extensions/StringExtensions.cs @@ -5,77 +5,66 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// StringExtensions class +/// +public static class StringExtensions { /// - /// StringExtensions class + /// Returns null if a string is empty or made of only whitespace characters, otherwise returns the string /// - public static class StringExtensions + /// string input + /// null if the string is wempty or made of only whitespace characters, otherwise returns the string + public static string NullIfWhiteSpace(this string s) { - /// - /// Returns null if a string is empty, otherwise returns the string - /// - /// string input - /// null if the string is emtpy, otherwise returns the string - public static string NullIfEmpty(this string s) - { - return string.IsNullOrEmpty(s) ? null : s; - } - - /// - /// Returns null if a string is empty or made of only whitespace characters, otherwise returns the string - /// - /// string input - /// null if the string is wempty or made of only whitespace characters, otherwise returns the string - public static string NullIfWhiteSpace(this string s) - { - return string.IsNullOrEmpty(s.Trim()) ? null : s; - } - - /// - /// Returns a replacement string if the input string is empty or made of only whitespace characters, otherwise returns the input string - /// - /// input string - /// string to replace with if input string is empty or whitespace - /// returns newString if s is null, emtpy, or made of whitespace characters, otherwise returns s - public static string ReplaceIfNullOrEmpty(this string s, string newString) - { - return string.IsNullOrEmpty(s) ? newString : s; - } - - /// - /// Overload for Contains that allows setting an explicit String Comparison - /// - /// Source String - /// String to check in Source String - /// Comparison parameters - /// true of string contains "toCheck" - public static bool Contains(this string source, string toCheck, StringComparison comp) - { - if (string.IsNullOrEmpty(source)) return false; - return source.IndexOf(toCheck, comp) >= 0; - } - - /// - /// Performs TrimStart() and TrimEnd() on source string - /// - /// String to Trim - /// Trimmed String - public static string TrimAll(this string source) - { - return string.IsNullOrEmpty(source) ? string.Empty : source.TrimStart().TrimEnd(); - } - - /// - /// Performs TrimStart(chars char[]) and TrimEnd(chars char[]) on source string. - /// - /// String to Trim - /// Char Array to trim from string - /// Trimmed String - public static string TrimAll(this string source, char[] chars) - { - return string.IsNullOrEmpty(source) ? string.Empty : source.TrimStart(chars).TrimEnd(chars); - } - + return string.IsNullOrEmpty(s.Trim()) ? null : s; } + + /// + /// Returns a replacement string if the input string is empty or made of only whitespace characters, otherwise returns the input string + /// + /// input string + /// string to replace with if input string is empty or whitespace + /// returns newString if s is null, emtpy, or made of whitespace characters, otherwise returns s + public static string ReplaceIfNullOrEmpty(this string s, string newString) + { + return string.IsNullOrEmpty(s) ? newString : s; + } + + /// + /// Overload for Contains that allows setting an explicit String Comparison + /// + /// Source String + /// String to check in Source String + /// Comparison parameters + /// true of string contains "toCheck" + public static bool Contains(this string source, string toCheck, StringComparison comp) + { + if (string.IsNullOrEmpty(source)) return false; + return source.IndexOf(toCheck, comp) >= 0; + } + + /// + /// Performs TrimStart() and TrimEnd() on source string + /// + /// String to Trim + /// Trimmed String + public static string TrimAll(this string source) + { + return string.IsNullOrEmpty(source) ? string.Empty : source.TrimStart().TrimEnd(); + } + + /// + /// Performs TrimStart(chars char[]) and TrimEnd(chars char[]) on source string. + /// + /// String to Trim + /// Char Array to trim from string + /// Trimmed String + public static string TrimAll(this string source, char[] chars) + { + return string.IsNullOrEmpty(source) ? string.Empty : source.TrimStart(chars).TrimEnd(chars); + } + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs index f1a0923e..6672532f 100644 --- a/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs @@ -2,29 +2,28 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines a class that is capable of loading device types +/// +public interface IDeviceFactory { /// - /// Defines the contract for IDeviceFactory + /// Gets the type of the factory associated with the current instance. /// - public interface IDeviceFactory - { - /// - /// Gets the type of the factory associated with the current instance. - /// - Type FactoryType { get; } + Type FactoryType { get; } - /// - /// Gets a list of type names associated with the current plugin. - /// - List TypeNames { get; } + /// + /// Gets a list of type names associated with the current plugin. + /// + List TypeNames { get; } - /// - /// Builds and returns an instance based on the provided configuration. - /// - /// The configuration settings used to initialize the device. This parameter cannot be null. - /// An instance configured according to the specified . - EssentialsDevice BuildDevice(DeviceConfig deviceConfig); - } + /// + /// Builds and returns an instance based on the provided configuration. + /// + /// The configuration settings used to initialize the device. This parameter cannot be null. + /// An instance configured according to the specified . + EssentialsDevice BuildDevice(DeviceConfig deviceConfig); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Factory/IProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/IProcessorExtensionDeviceFactory.cs index 57870721..b0d56508 100644 --- a/src/PepperDash.Essentials.Core/Factory/IProcessorExtensionDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/IProcessorExtensionDeviceFactory.cs @@ -4,16 +4,17 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Defines the contract for IProcessorExtensionDeviceFactory +/// +public interface IProcessorExtensionDeviceFactory { /// - /// Defines the contract for IProcessorExtensionDeviceFactory + /// Loads all the extension factories to the ProcessorExtensionDeviceFactory /// - public interface IProcessorExtensionDeviceFactory - { - /// - /// Loads all the extension factories to the ProcessorExtensionDeviceFactory - /// - void LoadFactories(); - } + void LoadFactories(); } + diff --git a/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs index 1d9f41e1..eb02f3e5 100644 --- a/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs @@ -7,8 +7,8 @@ using System; using System.Collections.Generic; using System.Linq; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Represents a ProcessorExtensionDeviceFactory @@ -164,8 +164,6 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.InnerException.StackTrace); return null; } - } - } } diff --git a/src/PepperDash.Essentials.Core/Factory/ReadyEventArgs.cs b/src/PepperDash.Essentials.Core/Factory/ReadyEventArgs.cs index 544ef5cc..23fa1d1a 100644 --- a/src/PepperDash.Essentials.Core/Factory/ReadyEventArgs.cs +++ b/src/PepperDash.Essentials.Core/Factory/ReadyEventArgs.cs @@ -1,45 +1,39 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using PepperDash.Essentials.Core; +namespace PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core + +/// +/// Represents a IsReadyEventArgs +/// +public class IsReadyEventArgs : EventArgs { /// - /// Represents a IsReadyEventArgs + /// Gets or sets the IsReady /// - public class IsReadyEventArgs : EventArgs - { - /// - /// Gets or sets the IsReady - /// - public bool IsReady { get; set; } - - /// - /// Constructor - /// - /// indicates if the object is ready - public IsReadyEventArgs(bool data) - { - IsReady = data; - } - } + public bool IsReady { get; set; } /// - /// Defines the contract for IHasReady + /// Constructor /// - public interface IHasReady + /// indicates if the object is ready + public IsReadyEventArgs(bool data) { - /// - /// Fires when the IsReady property changes - /// - event EventHandler IsReadyEvent; - - /// - /// indicates whether the object is ready - /// - bool IsReady { get; } + IsReady = data; } } + +/// +/// Defines the contract for IHasReady +/// +public interface IHasReady +{ + /// + /// Fires when the IsReady property changes + /// + event EventHandler IsReadyEvent; + + /// + /// indicates whether the object is ready + /// + bool IsReady { get; } +} diff --git a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedback.cs index ea222b7f..3aa75e22 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedback.cs @@ -5,195 +5,165 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// A Feedback whose output is derived from the return value of a provided Func. +/// +public class BoolFeedback : Feedback { /// - /// A Feedback whose output is derived from the return value of a provided Func. + /// Returns the current value of the feedback, derived from the ValueFunc. The ValueFunc is + /// evaluated whenever FireUpdate() is called /// - public class BoolFeedback : Feedback + public override bool BoolValue { get { return _BoolValue; } } + bool _BoolValue; + + /// + /// Fake value to be used in test mode + /// + public bool TestValue { get; private set; } + + /// + /// Func that evaluates on FireUpdate + /// + public Func ValueFunc { get; private set; } + + List LinkedInputSigs = new List(); + List LinkedComplementInputSigs = new List(); + + List LinkedCrestronFeedbacks = new List(); + + /// + /// Creates the feedback with the Func as described. + /// + /// + /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, + /// it will NOT reflect an actual value from a device until has been called + /// + /// Delegate to invoke when this feedback needs to be updated + public BoolFeedback(Func valueFunc) + : this(null, valueFunc) { - /// - /// Returns the current value of the feedback, derived from the ValueFunc. The ValueFunc is - /// evaluated whenever FireUpdate() is called - /// - public override bool BoolValue { get { return _BoolValue; } } - bool _BoolValue; + } - /// - /// Fake value to be used in test mode - /// - public bool TestValue { get; private set; } + /// + /// Creates the feedback with the Func as described. + /// + /// + /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, + /// it will NOT reflect an actual value from a device until has been called + /// + /// Key to find this Feedback + /// Delegate to invoke when this feedback needs to be updated + public BoolFeedback(string key, Func valueFunc) + : base(key) + { + ValueFunc = valueFunc; + } - /// - /// Gets or sets the ValueFunc - /// - public Func ValueFunc { get; private set; } + public void SetValueFunc(Func newFunc) + { + ValueFunc = newFunc; + } - List LinkedInputSigs = new List(); - List LinkedComplementInputSigs = new List(); - - List LinkedCrestronFeedbacks = new List(); - - /// - /// Creates the feedback with the Func as described. - /// - /// - /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, - /// it will NOT reflect an actual value from a device until has been called - /// - /// Delegate to invoke when this feedback needs to be updated - [Obsolete("use constructor with Key parameter. This constructor will be removed in a future version")] - public BoolFeedback(Func valueFunc) - : this(null, valueFunc) + public override void FireUpdate() + { + bool newValue = InTestMode ? TestValue : ValueFunc.Invoke(); + if (newValue != _BoolValue) { - } - - /// - /// Creates the feedback with the Func as described. - /// - /// - /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, - /// it will NOT reflect an actual value from a device until has been called - /// - /// Key to find this Feedback - /// Delegate to invoke when this feedback needs to be updated - public BoolFeedback(string key, Func valueFunc) - : base(key) - { - ValueFunc = valueFunc; - } - - /// - /// Sets the ValueFunc - /// - /// New function to set as the ValueFunc - public void SetValueFunc(Func newFunc) - { - ValueFunc = newFunc; - } - - /// - /// FireUpdate method - /// - /// - public override void FireUpdate() - { - bool newValue = InTestMode ? TestValue : ValueFunc.Invoke(); - if (newValue != _BoolValue) - { - _BoolValue = newValue; - LinkedInputSigs.ForEach(s => UpdateSig(s)); - LinkedComplementInputSigs.ForEach(s => UpdateComplementSig(s)); - OnOutputChange(newValue); - } - } - - /// - /// Links an input sig - /// - /// - /// - /// LinkInputSig method - /// - public void LinkInputSig(BoolInputSig sig) - { - LinkedInputSigs.Add(sig); - UpdateSig(sig); - } - - /// - /// Unlinks an inputs sig - /// - /// - /// - /// UnlinkInputSig method - /// - public void UnlinkInputSig(BoolInputSig sig) - { - LinkedInputSigs.Remove(sig); - } - - /// - /// Links an input sig to the complement value - /// - /// - public void LinkComplementInputSig(BoolInputSig sig) - { - LinkedComplementInputSigs.Add(sig); - UpdateComplementSig(sig); - } - - /// - /// Unlinks an input sig to the complement value - /// - /// - /// - /// UnlinkComplementInputSig method - /// - public void UnlinkComplementInputSig(BoolInputSig sig) - { - LinkedComplementInputSigs.Remove(sig); - } - - /// - /// Links a Crestron Feedback object - /// - /// - public void LinkCrestronFeedback(Crestron.SimplSharpPro.DeviceSupport.Feedback feedback) - { - LinkedCrestronFeedbacks.Add(feedback); - UpdateCrestronFeedback(feedback); - } - - /// - /// - /// - /// - /// - /// UnlinkCrestronFeedback method - /// - public void UnlinkCrestronFeedback(Crestron.SimplSharpPro.DeviceSupport.Feedback feedback) - { - LinkedCrestronFeedbacks.Remove(feedback); - } - - /// - /// ToString override - /// - /// - public override string ToString() - { - return (InTestMode ? "TEST -- " : "") + BoolValue.ToString(); - } - - /// - /// Puts this in test mode, sets the test value and fires an update. - /// - /// - /// - /// SetTestValue method - /// - public void SetTestValue(bool value) - { - TestValue = value; - InTestMode = true; - FireUpdate(); - } - - void UpdateSig(BoolInputSig sig) - { - sig.BoolValue = _BoolValue; - } - - void UpdateComplementSig(BoolInputSig sig) - { - sig.BoolValue = !_BoolValue; - } - - void UpdateCrestronFeedback(Crestron.SimplSharpPro.DeviceSupport.Feedback feedback) - { - feedback.State = _BoolValue; + _BoolValue = newValue; + LinkedInputSigs.ForEach(s => UpdateSig(s)); + LinkedComplementInputSigs.ForEach(s => UpdateComplementSig(s)); + OnOutputChange(newValue); } } + /// + /// Links an input sig + /// + /// + public void LinkInputSig(BoolInputSig sig) + { + LinkedInputSigs.Add(sig); + UpdateSig(sig); + } + + /// + /// Unlinks an inputs sig + /// + /// + public void UnlinkInputSig(BoolInputSig sig) + { + LinkedInputSigs.Remove(sig); + } + + /// + /// Links an input sig to the complement value + /// + /// + public void LinkComplementInputSig(BoolInputSig sig) + { + LinkedComplementInputSigs.Add(sig); + UpdateComplementSig(sig); + } + + /// + /// Unlinks an input sig to the complement value + /// + /// + public void UnlinkComplementInputSig(BoolInputSig sig) + { + LinkedComplementInputSigs.Remove(sig); + } + + /// + /// Links a Crestron Feedback object + /// + /// + public void LinkCrestronFeedback(Crestron.SimplSharpPro.DeviceSupport.Feedback feedback) + { + LinkedCrestronFeedbacks.Add(feedback); + UpdateCrestronFeedback(feedback); + } + + /// + /// + /// + /// + public void UnlinkCrestronFeedback(Crestron.SimplSharpPro.DeviceSupport.Feedback feedback) + { + LinkedCrestronFeedbacks.Remove(feedback); + } + + public override string ToString() + { + return (InTestMode ? "TEST -- " : "") + BoolValue.ToString(); + } + + /// + /// Puts this in test mode, sets the test value and fires an update. + /// + /// + public void SetTestValue(bool value) + { + TestValue = value; + InTestMode = true; + FireUpdate(); + } + + void UpdateSig(BoolInputSig sig) + { + sig.BoolValue = _BoolValue; + } + + void UpdateComplementSig(BoolInputSig sig) + { + sig.BoolValue = !_BoolValue; + } + + void UpdateCrestronFeedback(Crestron.SimplSharpPro.DeviceSupport.Feedback feedback) + { + feedback.State = _BoolValue; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackOneShot.cs b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackOneShot.cs index 6b0b39b8..d0d0083e 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackOneShot.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackOneShot.cs @@ -1,29 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro; +using Crestron.SimplSharp; namespace PepperDash.Essentials.Core { - /// - /// Represents a BoolFeedbackPulse - /// + /// + /// Represents a BoolFeedbackPulse + /// public class BoolFeedbackPulse { - /// - /// Gets or sets the TimeoutMs - /// + /// + /// Gets or sets the TimeoutMs + /// public uint TimeoutMs { get; set; } - /// - /// Gets or sets the CanRetrigger - /// + /// + /// Gets or sets the CanRetrigger + /// public bool CanRetrigger { get; set; } - /// - /// Gets or sets the Feedback - /// + /// + /// Gets or sets the Feedback + /// public BoolFeedback Feedback { get; private set; } CTimer Timer; @@ -68,12 +63,12 @@ namespace PepperDash.Essentials.Core Timer.Reset(TimeoutMs); } - /// - /// Cancel method - /// + /// + /// Cancel method + /// public void Cancel() { - if(Timer != null) + if (Timer != null) Timer.Reset(0); } } diff --git a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackPulseExtender.cs b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackPulseExtender.cs index 3a9dccde..87c6e9d7 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackPulseExtender.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackPulseExtender.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// A class that wraps a BoolFeedback with logic that extends it's true state for /// a time period after the value goes false. @@ -81,5 +81,4 @@ namespace PepperDash.Essentials.Core Feedback.FireUpdate(); Timer = null; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/BoolOutputLogicals.cs b/src/PepperDash.Essentials.Core/Feedbacks/BoolOutputLogicals.cs index 901b80dd..323a9be1 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/BoolOutputLogicals.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/BoolOutputLogicals.cs @@ -5,8 +5,8 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Abstract base class for BoolOutputLogicals @@ -98,7 +98,7 @@ namespace PepperDash.Essentials.Core public void ClearOutputs() { OutputsIn.Clear(); - Evaluate(); + Evaluate(); } /// @@ -190,5 +190,4 @@ namespace PepperDash.Essentials.Core ComputedValue = newValue; Output.FireUpdate(); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs index 0c756ace..e54fc864 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs @@ -1,10 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using Crestron.SimplSharp; -using Crestron.SimplSharpPro; - using PepperDash.Core; namespace PepperDash.Essentials.Core @@ -120,4 +115,4 @@ namespace PepperDash.Essentials.Core if (OutputChange != null) OutputChange(this, new FeedbackEventArgs(value)); } } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs index ed7517da..32b1abfb 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs @@ -4,22 +4,21 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Basically a List , with an indexer to find feedbacks by key name +/// +public class FeedbackCollection : List where T : Feedback { /// - /// Basically a List , with an indexer to find feedbacks by key name + /// Case-insensitive port lookup linked to feedbacks' keys /// - public class FeedbackCollection : List where T : Feedback + public T this[string key] { - /// - /// Case-insensitive port lookup linked to feedbacks' keys - /// - public T this[string key] + get { - get - { - return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - } + return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackEventArgs.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackEventArgs.cs index e22e9366..d2c1a815 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackEventArgs.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackEventArgs.cs @@ -4,93 +4,96 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a FeedbackEventArgs +/// +public class FeedbackEventArgs : EventArgs { /// - /// Represents a FeedbackEventArgs + /// Gets or sets the BoolValue /// - public class FeedbackEventArgs : EventArgs + public bool BoolValue { get; private set; } + + /// + /// Gets or sets the IntValue + /// + public int IntValue { get; private set; } + + /// + /// Gets or sets the UShortValue + /// + public ushort UShortValue { - /// - /// Gets or sets the BoolValue - /// - public bool BoolValue { get; private set; } - - /// - /// Gets or sets the IntValue - /// - public int IntValue { get; private set; } - - /// - /// Gets or sets the UShortValue - /// - public ushort UShortValue + get { - get - { - return (ushort)IntValue; - } - } - - /// - /// Gets or sets the StringValue - /// - public string StringValue { get; private set; } - - /// - /// Gets or sets the Type - /// - public eFeedbackEventType Type { get; private set; } - - /// - /// Constructor for BoolValue - /// - /// value to set - public FeedbackEventArgs(bool value) - { - BoolValue = value; - Type = eFeedbackEventType.TypeBool; - } - - /// - /// Constructor for IntValue - /// - /// value to set - public FeedbackEventArgs(int value) - { - IntValue = value; - Type = eFeedbackEventType.TypeInt; - } - - /// - /// Constructor for StringValue - /// - /// value to set - public FeedbackEventArgs(string value) - { - StringValue = value; - Type = eFeedbackEventType.TypeString; + return (ushort)IntValue; } } /// - /// Enumeration of eFeedbackEventType values + /// Gets or sets the StringValue /// - public enum eFeedbackEventType + public string StringValue { get; private set; } + + /// + /// Gets or sets the Type + /// + public eFeedbackEventType Type { get; private set; } + + /// + /// Constructor for BoolValue + /// + /// value to set + public FeedbackEventArgs(bool value) { - /// - /// Boolean type feedback event - /// - TypeBool, - - /// - /// Integer type feedback event - /// - TypeInt, - - /// - /// String type feedback event - /// - TypeString + BoolValue = value; + Type = eFeedbackEventType.TypeBool; } -} \ No newline at end of file + + /// + /// Constructor for IntValue + /// + /// value to set + public FeedbackEventArgs(int value) + { + IntValue = value; + Type = eFeedbackEventType.TypeInt; + } + + /// + /// Constructor for StringValue + /// + /// value to set + public FeedbackEventArgs(string value) + { + StringValue = value; + Type = eFeedbackEventType.TypeString; + } +} + +/// +/// Enumeration of eFeedbackEventType values +/// +public enum eFeedbackEventType +{ + /// + /// Boolean type feedback event + /// + TypeBool, + + /// + /// Integer type feedback event + /// + TypeInt, + + /// + /// String type feedback event + /// + TypeString +} + + + + diff --git a/src/PepperDash.Essentials.Core/Feedbacks/IntFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/IntFeedback.cs index 4dd0424b..1cf7a3bd 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/IntFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/IntFeedback.cs @@ -5,132 +5,98 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public class IntFeedback : Feedback { + public override int IntValue { get { return _IntValue; } } // ValueFunc.Invoke(); } } + int _IntValue; + public ushort UShortValue { get { return (ushort)_IntValue; } } + + //public override eCueType Type { get { return eCueType.Int; } } + + public int TestValue { get; private set; } + /// - /// Represents a IntFeedback + /// Func evaluated on FireUpdate /// - public class IntFeedback : Feedback + Func ValueFunc; + List LinkedInputSigs = new List(); + + /// + /// Creates the feedback with the Func as described. + /// + /// + /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, + /// it will NOT reflect an actual value from a device until has been called + /// + /// Delegate to invoke when this feedback needs to be updated + public IntFeedback(Func valueFunc) + : this(null, valueFunc) { - /// - /// Gets or sets the IntValue - /// - public override int IntValue { get { return _IntValue; } } // ValueFunc.Invoke(); } } - int _IntValue; - /// - /// Gets or sets the UShortValue - /// - public ushort UShortValue { get { return (ushort)_IntValue; } } + } - //public override eCueType Type { get { return eCueType.Int; } } + /// + /// Creates the feedback with the Func as described. + /// + /// + /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, + /// it will NOT reflect an actual value from a device until has been called + /// + /// Key to find this Feedback + /// Delegate to invoke when this feedback needs to be updated + public IntFeedback(string key, Func valueFunc) + : base(key) + { + ValueFunc = valueFunc; + } - /// - /// Gets or sets the TestValue - /// - public int TestValue { get; private set; } + public void SetValueFunc(Func newFunc) + { + ValueFunc = newFunc; + } - /// - /// Func evaluated on FireUpdate - /// - Func ValueFunc; - List LinkedInputSigs = new List(); - /// - /// Creates the feedback with the Func as described. - /// - /// - /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, - /// it will NOT reflect an actual value from a device until has been called - /// - /// Delegate to invoke when this feedback needs to be updated - [Obsolete("use constructor with Key parameter. This constructor will be removed in a future version")] - public IntFeedback(Func valueFunc) - : this(null, valueFunc) + public override void FireUpdate() + { + var newValue = InTestMode ? TestValue : ValueFunc.Invoke(); + if (newValue != _IntValue) { - } - - /// - /// Creates the feedback with the Func as described. - /// - /// - /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, - /// it will NOT reflect an actual value from a device until has been called - /// - /// Key to find this Feedback - /// Delegate to invoke when this feedback needs to be updated - public IntFeedback(string key, Func valueFunc) - : base(key) - { - ValueFunc = valueFunc; - } - - /// - /// Sets the ValueFunc - /// - /// function to set - public void SetValueFunc(Func newFunc) - { - ValueFunc = newFunc; - } - - - /// - /// FireUpdate method - /// - /// - public override void FireUpdate() - { - var newValue = InTestMode ? TestValue : ValueFunc.Invoke(); - if (newValue != _IntValue) - { - _IntValue = newValue; - LinkedInputSigs.ForEach(s => UpdateSig(s)); - OnOutputChange(newValue); - } - } - - /// - /// LinkInputSig method - /// - public void LinkInputSig(UShortInputSig sig) - { - LinkedInputSigs.Add(sig); - UpdateSig(sig); - } - - /// - /// UnlinkInputSig method - /// - public void UnlinkInputSig(UShortInputSig sig) - { - LinkedInputSigs.Remove(sig); - } - - /// - /// ToString method - /// - public override string ToString() - { - return (InTestMode ? "TEST -- " : "") + IntValue.ToString(); - } - - /// - /// Puts this in test mode, sets the test value and fires an update. - /// - /// - /// - /// SetTestValue method - /// - public void SetTestValue(int value) - { - TestValue = value; - InTestMode = true; - FireUpdate(); - } - - void UpdateSig(UShortInputSig sig) - { - sig.UShortValue = UShortValue; + _IntValue = newValue; + LinkedInputSigs.ForEach(s => UpdateSig(s)); + OnOutputChange(newValue); } } + + public void LinkInputSig(UShortInputSig sig) + { + LinkedInputSigs.Add(sig); + UpdateSig(sig); + } + + public void UnlinkInputSig(UShortInputSig sig) + { + LinkedInputSigs.Remove(sig); + } + + public override string ToString() + { + return (InTestMode ? "TEST -- " : "") + IntValue.ToString(); + } + + /// + /// Puts this in test mode, sets the test value and fires an update. + /// + /// + public void SetTestValue(int value) + { + TestValue = value; + InTestMode = true; + FireUpdate(); + } + + void UpdateSig(UShortInputSig sig) + { + sig.UShortValue = UShortValue; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs index 1bde5a08..6fe0be62 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs @@ -5,112 +5,81 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// To be used for serial data feedback where the event chain / asynchronicity must be maintained +/// and calculating the value based on a Func when it is needed will not suffice. +/// +public class SerialFeedback : Feedback { + public override string SerialValue { get { return _SerialValue; } } + string _SerialValue; + + //public override eCueType Type { get { return eCueType.Serial; } } + /// - /// To be used for serial data feedback where the event chain / asynchronicity must be maintained - /// and calculating the value based on a Func when it is needed will not suffice. + /// Used in testing. Set/Clear functions /// - public class SerialFeedback : Feedback + public string TestValue { get; private set; } + + List LinkedInputSigs = new List(); + + public SerialFeedback() { - /// - /// Gets the SerialValue - /// - public override string SerialValue { get { return _SerialValue; } } - string _SerialValue; + } - //public override eCueType Type { get { return eCueType.Serial; } } + public SerialFeedback(string key) + : base(key) + { + } - /// - /// Gets or sets the TestValue - /// - public string TestValue { get; private set; } + public override void FireUpdate() + { + throw new NotImplementedException("This feedback type does not use Funcs"); + } - List LinkedInputSigs = new List(); + public void FireUpdate(string newValue) + { + _SerialValue = newValue; + LinkedInputSigs.ForEach(s => UpdateSig(s, newValue)); + OnOutputChange(newValue); + } - /// - /// Constructor - /// - [Obsolete("use constructor with Key parameter. This constructor will be removed in a future version")] - public SerialFeedback() - { - } + public void LinkInputSig(StringInputSig sig) + { + LinkedInputSigs.Add(sig); + UpdateSig(sig); + } - /// - /// Constructor with Key parameter - /// - /// Key to find this Feedback - public SerialFeedback(string key) - : base(key) - { - } + public void UnlinkInputSig(StringInputSig sig) + { + LinkedInputSigs.Remove(sig); + } - /// - /// FireUpdate method - /// - /// - public override void FireUpdate() - { - throw new NotImplementedException("This feedback type does not use Funcs"); - } + public override string ToString() + { + return (InTestMode ? "TEST -- " : "") + SerialValue; + } - /// - /// FireUpdate method - /// - public void FireUpdate(string newValue) - { - _SerialValue = newValue; - LinkedInputSigs.ForEach(s => UpdateSig(s, newValue)); - OnOutputChange(newValue); - } + /// + /// Puts this in test mode, sets the test value and fires an update. + /// + /// + public void SetTestValue(string value) + { + TestValue = value; + InTestMode = true; + FireUpdate(TestValue); + } - /// - /// LinkInputSig method - /// - public void LinkInputSig(StringInputSig sig) - { - LinkedInputSigs.Add(sig); - UpdateSig(sig); - } + void UpdateSig(StringInputSig sig) + { + sig.StringValue = _SerialValue; + } - /// - /// UnlinkInputSig method - /// - public void UnlinkInputSig(StringInputSig sig) - { - LinkedInputSigs.Remove(sig); - } - - /// - /// ToString method - /// - public override string ToString() - { - return (InTestMode ? "TEST -- " : "") + SerialValue; - } - - /// - /// Puts this in test mode, sets the test value and fires an update. - /// - /// - /// - /// SetTestValue method - /// - public void SetTestValue(string value) - { - TestValue = value; - InTestMode = true; - FireUpdate(TestValue); - } - - void UpdateSig(StringInputSig sig) - { - sig.StringValue = _SerialValue; - } - - void UpdateSig(StringInputSig sig, string value) - { - sig.StringValue = value; - } + void UpdateSig(StringInputSig sig, string value) + { + sig.StringValue = value; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/StringFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/StringFeedback.cs index 4c419903..2eb18414 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/StringFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/StringFeedback.cs @@ -5,126 +5,98 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +public class StringFeedback : Feedback { + public override string StringValue { get { return _StringValue; } } // ValueFunc.Invoke(); } } + string _StringValue; /// - /// Represents a StringFeedback + /// Used in testing. Set/Clear functions /// - public class StringFeedback : Feedback + public string TestValue { get; private set; } + + /// + /// Evaluated on FireUpdate + /// + public Func ValueFunc { get; private set; } + List LinkedInputSigs = new List(); + + /// + /// Creates the feedback with the Func as described. + /// + /// + /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, + /// it will NOT reflect an actual value from a device until has been called + /// + /// Delegate to invoke when this feedback needs to be updated + public StringFeedback(Func valueFunc) + : this(null, valueFunc) { - /// - /// Gets or sets the StringValue - /// - public override string StringValue { get { return _StringValue; } } // ValueFunc.Invoke(); } } - string _StringValue; + } - /// - /// Gets or sets the TestValue - /// - public string TestValue { get; private set; } + /// + /// Creates the feedback with the Func as described. + /// + /// + /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, + /// it will NOT reflect an actual value from a device until has been called + /// + /// Key to find this Feedback + /// Delegate to invoke when this feedback needs to be updated + public StringFeedback(string key, Func valueFunc) + : base(key) + { + ValueFunc = valueFunc; + } - /// - /// Gets or sets the ValueFunc - /// - public Func ValueFunc { get; private set; } - List LinkedInputSigs = new List(); + public void SetValueFunc(Func newFunc) + { + ValueFunc = newFunc; + } - /// - /// Creates the feedback with the Func as described. - /// - /// - /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, - /// it will NOT reflect an actual value from a device until has been called - /// - /// Delegate to invoke when this feedback needs to be updated - [Obsolete("use constructor with Key parameter. This constructor will be removed in a future version")] - public StringFeedback(Func valueFunc) - : this(null, valueFunc) + public override void FireUpdate() + { + var newValue = InTestMode ? TestValue : ValueFunc.Invoke(); + if (newValue != _StringValue) { - } - - /// - /// Creates the feedback with the Func as described. - /// - /// - /// While the linked sig value will be updated with the current value stored when it is linked to a EISC Bridge, - /// it will NOT reflect an actual value from a device until has been called - /// - /// Key to find this Feedback - /// Delegate to invoke when this feedback needs to be updated - public StringFeedback(string key, Func valueFunc) - : base(key) - { - ValueFunc = valueFunc; - } - - /// - /// Sets the ValueFunc - /// - /// function to set - public void SetValueFunc(Func newFunc) - { - ValueFunc = newFunc; - } - - /// - /// FireUpdate method - /// - /// - public override void FireUpdate() - { - var newValue = InTestMode ? TestValue : ValueFunc.Invoke(); - if (newValue != _StringValue) - { - _StringValue = newValue; - LinkedInputSigs.ForEach(s => UpdateSig(s)); - OnOutputChange(newValue); - } - } - - /// - /// LinkInputSig method - /// - public void LinkInputSig(StringInputSig sig) - { - LinkedInputSigs.Add(sig); - UpdateSig(sig); - } - - /// - /// UnlinkInputSig method - /// - public void UnlinkInputSig(StringInputSig sig) - { - LinkedInputSigs.Remove(sig); - } - - /// - /// ToString method - /// - public override string ToString() - { - return (InTestMode ? "TEST -- " : "") + StringValue; - } - - /// - /// Puts this in test mode, sets the test value and fires an update. - /// - /// - /// - /// SetTestValue method - /// - public void SetTestValue(string value) - { - TestValue = value; - InTestMode = true; - FireUpdate(); - } - - void UpdateSig(StringInputSig sig) - { - sig.StringValue = _StringValue; + _StringValue = newValue; + LinkedInputSigs.ForEach(s => UpdateSig(s)); + OnOutputChange(newValue); } } + + public void LinkInputSig(StringInputSig sig) + { + LinkedInputSigs.Add(sig); + UpdateSig(sig); + } + + public void UnlinkInputSig(StringInputSig sig) + { + LinkedInputSigs.Remove(sig); + } + + public override string ToString() + { + return (InTestMode ? "TEST -- " : "") + StringValue; + } + + /// + /// Puts this in test mode, sets the test value and fires an update. + /// + /// + public void SetTestValue(string value) + { + TestValue = value; + InTestMode = true; + FireUpdate(); + } + + void UpdateSig(StringInputSig sig) + { + sig.StringValue = _StringValue; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/File/FileIO.cs b/src/PepperDash.Essentials.Core/File/FileIO.cs index df1c0750..25a6d87c 100644 --- a/src/PepperDash.Essentials.Core/File/FileIO.cs +++ b/src/PepperDash.Essentials.Core/File/FileIO.cs @@ -34,43 +34,42 @@ namespace PepperDash.Essentials.Core /// /// /// - /// - /// GetFiles method - /// - public static FileInfo[] GetFiles(string fileName) - { - string fullFilePath = Global.FilePathPrefix + fileName; - DirectoryInfo dirInfo = new DirectoryInfo(Path.GetDirectoryName(fullFilePath)); - var files = dirInfo.GetFiles(Path.GetFileName(fullFilePath)); - Debug.LogMessage(LogEventLevel.Information, "FileIO found: {0}, {1}", files.Count(), fullFilePath); - if (files.Count() > 0) - { - return files; - } - else - { - return null; - } - } + public static FileInfo[] GetFiles(string fileName) + { + string fullFilePath = Global.FilePathPrefix + fileName; + DirectoryInfo dirInfo = new DirectoryInfo(Path.GetDirectoryName(fullFilePath)); + var files = dirInfo.GetFiles(Path.GetFileName(fullFilePath)); + Debug.LogMessage(LogEventLevel.Information, "FileIO found: {0}, {1}", files.Count(), fullFilePath); + if (files.Count() > 0) + { + return files; + } + else + { + return null; + } + } - /// - /// GetFile method - /// - public static FileInfo GetFile(string fileName) - { - string fullFilePath = Global.FilePathPrefix + fileName; - DirectoryInfo dirInfo = new DirectoryInfo(Path.GetDirectoryName(fullFilePath)); - var files = dirInfo.GetFiles(Path.GetFileName(fullFilePath)); - Debug.LogMessage(LogEventLevel.Information, "FileIO found: {0}, {1}", files.Count(), fullFilePath); - if (files.Count() > 0) - { - return files.FirstOrDefault(); - } - else - { - return null; - } - } + /// + /// Get the full file info from a path/filename, can include wildcards. + /// + /// + /// + public static FileInfo GetFile(string fileName) + { + string fullFilePath = Global.FilePathPrefix + fileName; + DirectoryInfo dirInfo = new DirectoryInfo(Path.GetDirectoryName(fullFilePath)); + var files = dirInfo.GetFiles(Path.GetFileName(fullFilePath)); + Debug.LogMessage(LogEventLevel.Information, "FileIO found: {0}, {1}", files.Count(), fullFilePath); + if (files.Count() > 0) + { + return files.FirstOrDefault(); + } + else + { + return null; + } + } /// @@ -78,9 +77,9 @@ namespace PepperDash.Essentials.Core /// /// /// - /// - /// ReadDataFromFile method - /// + /// + /// ReadDataFromFile method + /// public static string ReadDataFromFile(string fileName) { try @@ -94,15 +93,15 @@ namespace PepperDash.Essentials.Core } } - /// - /// Get the data with fileInfo object - /// - /// - /// - /// - /// ReadDataFromFile method - /// - public static string ReadDataFromFile(FileInfo file) + /// + /// Get the data with fileInfo object + /// + /// + /// + /// + /// ReadDataFromFile method + /// + public static string ReadDataFromFile(FileInfo file) { try { @@ -129,7 +128,7 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Information, "FileIO Unable to enter FileLock"); return ""; } - + } catch (Exception e) { @@ -145,9 +144,9 @@ namespace PepperDash.Essentials.Core } - /// - /// ReadDataFromFileASync method - /// + /// + /// ReadDataFromFileASync method + /// public static void ReadDataFromFileASync(string fileName) { try @@ -160,9 +159,9 @@ namespace PepperDash.Essentials.Core } } - /// - /// ReadDataFromFileASync method - /// + /// + /// ReadDataFromFileASync method + /// public static void ReadDataFromFileASync(FileInfo file) { try @@ -230,7 +229,7 @@ namespace PepperDash.Essentials.Core public static void WriteDataToFile(string data, string filePath) { Thread _WriteFileThread; - _WriteFileThread = new Thread((O) => _WriteFileMethod(data, Global.FilePathPrefix + "/" + filePath), null, Thread.eThreadStartOptions.CreateSuspended); + _WriteFileThread = new Thread((O) => _WriteFileMethod(data, Global.FilePathPrefix + "/" + filePath), null, Thread.eThreadStartOptions.CreateSuspended); _WriteFileThread.Priority = Thread.eThreadPriority.LowestPriority; _WriteFileThread.Start(); Debug.LogMessage(LogEventLevel.Information, "New WriteFile Thread"); @@ -246,7 +245,7 @@ namespace PepperDash.Essentials.Core if (fileLock.TryEnter()) { - using (StreamWriter sw = new StreamWriter(filePath)) + using (StreamWriter sw = new StreamWriter(filePath)) { sw.Write(data); sw.Flush(); @@ -277,16 +276,16 @@ namespace PepperDash.Essentials.Core /// /// /// - /// - /// FileIoUnitTest method - /// + /// + /// FileIoUnitTest method + /// public static bool FileIoUnitTest() { var testData = "Testing FileIO"; FileIO.WriteDataToFile(testData, "\\user\\FileIOTest.pdt"); var file = FileIO.GetFile("\\user\\*FileIOTest*"); - + var readData = FileIO.ReadDataFromFile(file); Debug.LogMessage(LogEventLevel.Information, "Returned {0}", readData); File.Delete(file.FullName); @@ -301,9 +300,9 @@ namespace PepperDash.Essentials.Core } } - /// - /// Represents a FileEventArgs - /// + /// + /// Represents a FileEventArgs + /// public class FileEventArgs { /// @@ -311,9 +310,9 @@ namespace PepperDash.Essentials.Core /// /// public FileEventArgs(string data) { Data = data; } - /// - /// Gets or sets the Data - /// + /// + /// Gets or sets the Data + /// public string Data { get; private set; } // readonly } diff --git a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs index ba638623..d31f4cdf 100644 --- a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs @@ -7,227 +7,227 @@ using Crestron.SimplSharp; using PepperDash.Essentials.Core.Bridges; -namespace PepperDash.Essentials.Core.Fusion +namespace PepperDash.Essentials.Core.Fusion; + +/// +/// Represents a EssentialsHuddleSpaceRoomFusionRoomJoinMap +/// +public class EssentialsHuddleSpaceRoomFusionRoomJoinMap : JoinMapBaseAdvanced { + + // Processor Attributes /// - /// Represents a EssentialsHuddleSpaceRoomFusionRoomJoinMap + /// Processor IP 1 /// - public class EssentialsHuddleSpaceRoomFusionRoomJoinMap : JoinMapBaseAdvanced + [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 = 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 = 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 = 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 = 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 + /// + /// Join this join map will start at + public EssentialsHuddleSpaceRoomFusionRoomJoinMap(uint joinStart) + : base(joinStart, typeof(EssentialsHuddleSpaceRoomFusionRoomJoinMap)) { - // 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 }); + /// + /// Constructor to use when extending this Join map + /// + /// Join this join map will start at + /// Type of the child join map + public EssentialsHuddleSpaceRoomFusionRoomJoinMap(uint joinStart, Type type) : base(joinStart, type) + { + } +} - /// - /// 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 = 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 = 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 = 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 = 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 - /// - /// Join this join map will start at - public EssentialsHuddleSpaceRoomFusionRoomJoinMap(uint joinStart) - : base(joinStart, typeof(EssentialsHuddleSpaceRoomFusionRoomJoinMap)) - { - - } - - /// - /// Constructor to use when extending this Join map - /// - /// Join this join map will start at - /// Type of the child join map - public EssentialsHuddleSpaceRoomFusionRoomJoinMap(uint joinStart, Type type) : base(joinStart, type) - { - } - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs b/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs index 12f5d7e4..f4493fa5 100644 --- a/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs +++ b/src/PepperDash.Essentials.Core/Fusion/FusionCustomPropertiesBridge.cs @@ -8,129 +8,89 @@ using PepperDash.Core; using PepperDash.Essentials.Core.Devices; using Serilog.Events; -namespace PepperDash.Essentials.Core.Fusion +namespace PepperDash.Essentials.Core.Fusion; + +/// +/// Handles mapping Fusion Custom Property values to system properties +/// +public class FusionCustomPropertiesBridge { + /// - /// Handles mapping Fusion Custom Property values to system properties + /// Evaluates the room info and custom properties from Fusion and updates the system properties aa needed /// - public class FusionCustomPropertiesBridge + /// The room associated with this Fusion instance + /// The room information from Fusion + /// + public void EvaluateRoomInfo(IEssentialsRoom room, RoomInformation roomInfo, bool useFusionRoomName) { + var reconfigurableDevices = DeviceManager.AllDevices.Where(d => d is ReconfigurableDevice); - /// - /// Evaluates the room info and custom properties from Fusion and updates the system properties aa needed - /// - /// The room associated with this Fusion instance - /// The room information from Fusion - /// - public void EvaluateRoomInfo(IEssentialsRoom room, RoomInformation roomInfo, bool useFusionRoomName) + foreach (var device in reconfigurableDevices) { - try + // Get the current device config so new values can be overwritten over existing + var deviceConfig = (device as ReconfigurableDevice).Config; + + if (device is IEssentialsRoom) { - var reconfigurableDevices = DeviceManager.AllDevices.OfType(); - - foreach (var device in reconfigurableDevices) - { - // Get the current device config so new values can be overwritten over existing - var deviceConfig = device.Config; - - if (device is IEssentialsRoom) - { - // Skipping room name as this will affect ALL room instances in the configuration and cause unintended consequences when multiple rooms are present and multiple Fusion instances are used - continue; - } - - if (device is RoomOnToDefaultSourceWhenOccupied) - { - Debug.LogMessage(LogEventLevel.Debug, "Mapping Room on via Occupancy values from Fusion"); - - var devProps = JsonConvert.DeserializeObject(deviceConfig.Properties.ToString()); - - var enableFeature = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupied")); - if (enableFeature != null) - devProps.EnableRoomOnWhenOccupied = bool.Parse(enableFeature.CustomFieldValue); - - var enableTime = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("RoomOnWhenOccupiedStartTime")); - if (enableTime != null) - devProps.OccupancyStartTime = enableTime.CustomFieldValue; - - var disableTime = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("RoomOnWhenOccupiedEndTime")); - if (disableTime != null) - devProps.OccupancyEndTime = disableTime.CustomFieldValue; - - var enableSunday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedSun")); - if (enableSunday != null) - devProps.EnableSunday = bool.Parse(enableSunday.CustomFieldValue); - - var enableMonday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedMon")); - if (enableMonday != null) - devProps.EnableMonday = bool.Parse(enableMonday.CustomFieldValue); - - var enableTuesday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedTue")); - if (enableTuesday != null) - devProps.EnableTuesday = bool.Parse(enableTuesday.CustomFieldValue); - - var enableWednesday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedWed")); - if (enableWednesday != null) - devProps.EnableWednesday = bool.Parse(enableWednesday.CustomFieldValue); - - var enableThursday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedThu")); - if (enableThursday != null) - devProps.EnableThursday = bool.Parse(enableThursday.CustomFieldValue); - - var enableFriday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedFri")); - if (enableFriday != null) - devProps.EnableFriday = bool.Parse(enableFriday.CustomFieldValue); - - var enableSaturday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedSat")); - if (enableSaturday != null) - devProps.EnableSaturday = bool.Parse(enableSaturday.CustomFieldValue); - - deviceConfig.Properties = JToken.FromObject(devProps); - } - - // Set the config on the device - device.SetConfig(deviceConfig); - } - - if (!(room is ReconfigurableDevice reconfigurable)) - { - Debug.LogWarning("FusionCustomPropertiesBridge: Room is not a ReconfigurableDevice. Cannot map custom properties."); - return; - } - - var roomConfig = reconfigurable.Config; - - var updateConfig = false; - - // Set the room name - if (!string.IsNullOrEmpty(roomInfo.Name) && useFusionRoomName) - { - Debug.LogDebug("Current Room Name: {currentName}. New Room Name: {fusionName}", roomConfig.Name, roomInfo.Name); - // Set the name in config - roomConfig.Name = roomInfo.Name; - updateConfig = true; - - Debug.LogDebug("Room Name Successfully Changed."); - } - - // Set the help message - var helpMessage = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("RoomHelpMessage")); - if (helpMessage != null) - { - roomConfig.Properties["helpMessage"] = helpMessage.CustomFieldValue; - updateConfig = true; - } - - if (updateConfig) - { - reconfigurable.SetConfig(roomConfig); - } + // Skipping room name as this will affect ALL room instances in the configuration and cause unintended consequences when multiple rooms are present and multiple Fusion instances are used + continue; } - catch (Exception e) + + if (device is RoomOnToDefaultSourceWhenOccupied) { - Debug.LogError("FusionCustomPropetiesBridge: Exception mapping properties for {roomKey}: {message}", room.Key, e.Message); - Debug.LogDebug(e, "Stack Trace: "); + Debug.LogMessage(LogEventLevel.Debug, "Mapping Room on via Occupancy values from Fusion"); + + var devProps = JsonConvert.DeserializeObject(deviceConfig.Properties.ToString()); + + var enableFeature = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupied")); + if (enableFeature != null) + devProps.EnableRoomOnWhenOccupied = bool.Parse(enableFeature.CustomFieldValue); + + var enableTime = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("RoomOnWhenOccupiedStartTime")); + if (enableTime != null) + devProps.OccupancyStartTime = enableTime.CustomFieldValue; + + var disableTime = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("RoomOnWhenOccupiedEndTime")); + if (disableTime != null) + devProps.OccupancyEndTime = disableTime.CustomFieldValue; + + var enableSunday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedSun")); + if (enableSunday != null) + devProps.EnableSunday = bool.Parse(enableSunday.CustomFieldValue); + + var enableMonday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedMon")); + if (enableMonday != null) + devProps.EnableMonday = bool.Parse(enableMonday.CustomFieldValue); + + var enableTuesday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedTue")); + if (enableTuesday != null) + devProps.EnableTuesday = bool.Parse(enableTuesday.CustomFieldValue); + + var enableWednesday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedWed")); + if (enableWednesday != null) + devProps.EnableWednesday = bool.Parse(enableWednesday.CustomFieldValue); + + var enableThursday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedThu")); + if (enableThursday != null) + devProps.EnableThursday = bool.Parse(enableThursday.CustomFieldValue); + + var enableFriday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedFri")); + if (enableFriday != null) + devProps.EnableFriday = bool.Parse(enableFriday.CustomFieldValue); + + var enableSaturday = roomInfo.FusionCustomProperties.FirstOrDefault(p => p.ID.Equals("EnRoomOnWhenOccupiedSat")); + if (enableSaturday != null) + devProps.EnableSaturday = bool.Parse(enableSaturday.CustomFieldValue); + + deviceConfig.Properties = JToken.FromObject(devProps); } + + + // Set the config on the device + (device as ReconfigurableDevice).SetConfig(deviceConfig); } + + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/FusionEventHandlers.cs b/src/PepperDash.Essentials.Core/Fusion/FusionEventHandlers.cs index 6f374237..c6f766ea 100644 --- a/src/PepperDash.Essentials.Core/Fusion/FusionEventHandlers.cs +++ b/src/PepperDash.Essentials.Core/Fusion/FusionEventHandlers.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; + namespace PepperDash.Essentials.Core.Fusion { diff --git a/src/PepperDash.Essentials.Core/Fusion/FusionProcessorQueries.cs b/src/PepperDash.Essentials.Core/Fusion/FusionProcessorQueries.cs index 86ed62f3..aefe37ac 100644 --- a/src/PepperDash.Essentials.Core/Fusion/FusionProcessorQueries.cs +++ b/src/PepperDash.Essentials.Core/Fusion/FusionProcessorQueries.cs @@ -1,73 +1,67 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.Generic; using Crestron.SimplSharp; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.Fusion +namespace PepperDash.Essentials.Core.Fusion; + +/// +/// When created, runs progcomments on every slot and stores the program names in a list +/// +public class ProcessorProgReg { /// - /// When created, runs progcomments on every slot and stores the program names in a list + /// Gets the processor program registry /// - public class ProcessorProgReg + /// + public static Dictionary GetProcessorProgReg() { - //public static Dictionary Programs { get; private set; } - - /// - /// Gets the processor program registry - /// - /// - public static Dictionary GetProcessorProgReg() + var programs = new Dictionary(); + for (int i = 1; i <= Global.ControlSystem.NumProgramsSupported; i++) { - var programs = new Dictionary(); - for (int i = 1; i <= Global.ControlSystem.NumProgramsSupported; i++) + string response = null; + var success = CrestronConsole.SendControlSystemCommand("progcomments:" + i, ref response); + var item = new ProcessorProgramItem(); + if (!success) + item.Name = "Error: PROGCOMMENTS failed"; + else { - string response = null; - var success = CrestronConsole.SendControlSystemCommand("progcomments:" + i, ref response); - var item = new ProcessorProgramItem(); - if (!success) - item.Name = "Error: PROGCOMMENTS failed"; + if (response.ToLower().Contains("bad or incomplete")) + item.Name = ""; else { - if (response.ToLower().Contains("bad or incomplete")) - item.Name = ""; - else + var startPos = response.IndexOf("Program File"); + var colonPos = response.IndexOf(":", startPos) + 1; + var endPos = response.IndexOf(CrestronEnvironment.NewLine, colonPos); + item.Name = response.Substring(colonPos, endPos - colonPos).Trim(); + item.Exists = true; + if (item.Name.Contains(".dll")) { - var startPos = response.IndexOf("Program File"); - var colonPos = response.IndexOf(":", startPos) + 1; - var endPos = response.IndexOf(CrestronEnvironment.NewLine, colonPos); - item.Name = response.Substring(colonPos, endPos - colonPos).Trim(); - item.Exists = true; - if (item.Name.Contains(".dll")) - { - startPos = response.IndexOf("Compiler Revision"); - colonPos = response.IndexOf(":", startPos) + 1; - endPos = response.IndexOf(CrestronEnvironment.NewLine, colonPos); - item.Name = item.Name + "_v" + response.Substring(colonPos, endPos - colonPos).Trim(); - } + startPos = response.IndexOf("Compiler Revision"); + colonPos = response.IndexOf(":", startPos) + 1; + endPos = response.IndexOf(CrestronEnvironment.NewLine, colonPos); + item.Name = item.Name + "_v" + response.Substring(colonPos, endPos - colonPos).Trim(); } } - programs[i] = item; - Debug.LogMessage(LogEventLevel.Debug, "Program {0}: {1}", i, item.Name); } - return programs; + programs[i] = item; + Debug.LogMessage(LogEventLevel.Debug, "Program {0}: {1}", i, item.Name); } + return programs; } +} +/// +/// Represents a ProcessorProgramItem +/// +public class ProcessorProgramItem +{ /// - /// Represents a ProcessorProgramItem + /// Gets or sets the Exists /// - public class ProcessorProgramItem - { - /// - /// Gets or sets the Exists - /// - public bool Exists { get; set; } - /// - /// Gets or sets the Name - /// - public string Name { get; set; } - } -} \ No newline at end of file + public bool Exists { get; set; } + /// + /// Gets or sets the Name + /// + public string Name { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Fusion/FusionRviDataClasses.cs b/src/PepperDash.Essentials.Core/Fusion/FusionRviDataClasses.cs index cef4926f..ba53564a 100644 --- a/src/PepperDash.Essentials.Core/Fusion/FusionRviDataClasses.cs +++ b/src/PepperDash.Essentials.Core/Fusion/FusionRviDataClasses.cs @@ -8,828 +8,492 @@ using Crestron.SimplSharpPro.Fusion; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.Fusion +namespace PepperDash.Essentials.Core.Fusion; + +// Helper Classes for GUIDs + +/// +/// Stores GUIDs to be written to a file in NVRAM +/// +public class FusionRoomGuids { - // Helper Classes for GUIDs + public string RoomName { get; set; } + public uint IpId { get; set; } + public string RoomGuid { get; set; } + public FusionOccupancySensorAsset OccupancyAsset { get; set; } + public Dictionary StaticAssets { get; set; } - /// - /// Stores GUIDs to be written to a file in NVRAM - /// - public class FusionRoomGuids + public FusionRoomGuids() { - /// - /// Gets or sets the RoomName - /// - public string RoomName { get; set; } + StaticAssets = new Dictionary(); + OccupancyAsset = new FusionOccupancySensorAsset(); + } - /// - /// Gets or sets the IpId - /// - public uint IpId { get; set; } + public FusionRoomGuids(string roomName, uint ipId, string roomGuid, Dictionary staticAssets) + { + RoomName = roomName; + IpId = ipId; + RoomGuid = roomGuid; - /// - /// Gets or sets the RoomGuid - /// - public string RoomGuid { get; set; } + StaticAssets = staticAssets; + OccupancyAsset = new FusionOccupancySensorAsset(); + } - /// - /// Gets or sets the OccupancyAsset - /// - public FusionOccupancySensorAsset OccupancyAsset { get; set; } - - /// - /// Gets or sets the StaticAssets - /// - public Dictionary StaticAssets { get; set; } - - /// - /// FusionRoomGuids constructor - /// - public FusionRoomGuids() - { - StaticAssets = new Dictionary(); - OccupancyAsset = new FusionOccupancySensorAsset(); - } - - /// - /// FusionRoomGuids constructor - /// - /// name of the fusion room - /// ipID of the fusion room - /// room GUID - /// dictionary of assets - public FusionRoomGuids(string roomName, uint ipId, string roomGuid, Dictionary staticAssets) - { - RoomName = roomName; - IpId = ipId; - RoomGuid = roomGuid; - - StaticAssets = staticAssets; - OccupancyAsset = new FusionOccupancySensorAsset(); - } - - /// - /// FusionRoomGuids constructor - /// - /// name of the fusion room - /// ipID of the fusion room - /// room GUID - /// dictionary of assets - /// occupancy asset - public FusionRoomGuids(string roomName, uint ipId, string roomGuid, Dictionary staticAssets, FusionOccupancySensorAsset occAsset) - { - RoomName = roomName; - IpId = ipId; - RoomGuid = roomGuid; - - StaticAssets = staticAssets; - OccupancyAsset = occAsset; - } - - /// - /// Generates a new room GUID prefixed by the program slot number and NIC MAC address - /// - /// - /// - /// - /// GenerateNewRoomGuid method - /// - public string GenerateNewRoomGuid(uint progSlot, string mac) - { - Guid roomGuid = Guid.NewGuid(); - - return string.Format("{0}-{1}-{2}", progSlot, mac, roomGuid.ToString()); - } - - - /// - /// Adds an asset to the StaticAssets collection and returns the new asset - /// - /// - /// - /// - /// - /// - /// - public FusionAsset AddStaticAsset(FusionRoom room, int uid, string assetName, string type, string instanceId) - { - var slotNum = GetNextAvailableAssetNumber(room); - - Debug.LogMessage(LogEventLevel.Verbose, "Adding Fusion Asset: {0} of Type: {1} at Slot Number: {2} with GUID: {3}", assetName, type, slotNum, instanceId); - - var tempAsset = new FusionAsset(slotNum, assetName, type, instanceId); - - StaticAssets.Add(uid, tempAsset); - - return tempAsset; - } - - /// - /// Returns the next available slot number in the Fusion UserConfigurableAssetDetails collection - /// - /// - /// - /// - /// GetNextAvailableAssetNumber method - /// - public static uint GetNextAvailableAssetNumber(FusionRoom room) - { - uint slotNum = 0; - - foreach (var item in room.UserConfigurableAssetDetails) - { - if(item.Number > slotNum) - slotNum = item.Number; - } - - if (slotNum < 5) - { - slotNum = 5; - } - else - slotNum = slotNum + 1; - - Debug.LogMessage(LogEventLevel.Verbose, "#Next available fusion asset number is: {0}", slotNum); - - return slotNum; - } + public FusionRoomGuids(string roomName, uint ipId, string roomGuid, Dictionary staticAssets, FusionOccupancySensorAsset occAsset) + { + RoomName = roomName; + IpId = ipId; + RoomGuid = roomGuid; + StaticAssets = staticAssets; + OccupancyAsset = occAsset; } /// - /// Represents a FusionOccupancySensorAsset + /// Generates a new room GUID prefixed by the program slot number and NIC MAC address /// - public class FusionOccupancySensorAsset + /// + /// + public string GenerateNewRoomGuid(uint progSlot, string mac) { - // SlotNumber fixed at 4 + Guid roomGuid = Guid.NewGuid(); - /// - /// Gets or sets the SlotNumber - /// - public uint SlotNumber { get { return 4; } } - /// - /// Gets or sets the Name - /// - public string Name { get { return "Occupancy Sensor"; } } - /// - /// Gets or sets the Type - /// - public eAssetType Type { get; set; } - /// - /// Gets or sets the InstanceId - /// - public string InstanceId { get; set; } + return string.Format("{0}-{1}-{2}", progSlot, mac, roomGuid.ToString()); + } - /// - /// Default constructor - /// - public FusionOccupancySensorAsset() + + /// + /// Adds an asset to the StaticAssets collection and returns the new asset + /// + /// + /// + /// + /// + /// + /// + public FusionAsset AddStaticAsset(FusionRoom room, int uid, string assetName, string type, string instanceId) + { + var slotNum = GetNextAvailableAssetNumber(room); + + Debug.LogMessage(LogEventLevel.Verbose, "Adding Fusion Asset: {0} of Type: {1} at Slot Number: {2} with GUID: {3}", assetName, type, slotNum, instanceId); + + var tempAsset = new FusionAsset(slotNum, assetName, type, instanceId); + + StaticAssets.Add(uid, tempAsset); + + return tempAsset; + } + + /// + /// Returns the next available slot number in the Fusion UserConfigurableAssetDetails collection + /// + /// + /// + public static uint GetNextAvailableAssetNumber(FusionRoom room) + { + uint slotNum = 0; + + foreach (var item in room.UserConfigurableAssetDetails) { + if(item.Number > slotNum) + slotNum = item.Number; } - /// - /// FusionOccupancySensorAsset constructor - /// - /// asset type - public FusionOccupancySensorAsset(eAssetType type) + if (slotNum < 5) { - Type = type; + slotNum = 5; + } + else + slotNum = slotNum + 1; + Debug.LogMessage(LogEventLevel.Verbose, "#Next available fusion asset number is: {0}", slotNum); + + return slotNum; + } + +} + +public class FusionOccupancySensorAsset +{ + // SlotNumber fixed at 4 + + public uint SlotNumber { get { return 4; } } + public string Name { get { return "Occupancy Sensor"; } } + public eAssetType Type { get; set; } + public string InstanceId { get; set; } + + public FusionOccupancySensorAsset() + { + } + + public FusionOccupancySensorAsset(eAssetType type) + { + Type = type; + + InstanceId = Guid.NewGuid().ToString(); + } +} + +public class FusionAsset +{ + public uint SlotNumber { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string InstanceId { get;set; } + + public FusionAsset() + { + + } + + public FusionAsset(uint slotNum, string assetName, string type, string instanceId) + { + SlotNumber = slotNum; + Name = assetName; + Type = type; + if (string.IsNullOrEmpty(instanceId)) + { InstanceId = Guid.NewGuid().ToString(); } + else + { + InstanceId = instanceId; + } } +} + +//*************************************************************************************************** + +public class RoomSchedule +{ + public List Meetings { get; set; } + + public RoomSchedule() + { + Meetings = new List(); + } +} + +//**************************************************************************************************** +// Helper Classes for XML API + +/// +/// Data needed to request the local time from the Fusion server +/// +public class LocalTimeRequest +{ + public string RequestID { get; set; } +} + +/// +/// All the data needed for a full schedule request in a room +/// +/// //[XmlRoot(ElementName = "RequestSchedule")] +public class RequestSchedule +{ + //[XmlElement(ElementName = "RequestID")] + public string RequestID { get; set; } + //[XmlElement(ElementName = "RoomID")] + public string RoomID { get; set; } + //[XmlElement(ElementName = "Start")] + public DateTime Start { get; set; } + //[XmlElement(ElementName = "HourSpan")] + public double HourSpan { get; set; } + + public RequestSchedule(string requestID, string roomID) + { + RequestID = requestID; + RoomID = roomID; + Start = DateTime.Now; + HourSpan = 24; + } +} + + +//[XmlRoot(ElementName = "RequestAction")] +public class RequestAction +{ + //[XmlElement(ElementName = "RequestID")] + public string RequestID { get; set; } + //[XmlElement(ElementName = "RoomID")] + public string RoomID { get; set; } + //[XmlElement(ElementName = "ActionID")] + public string ActionID { get; set; } + //[XmlElement(ElementName = "Parameters")] + public List Parameters { get; set; } + + public RequestAction(string roomID, string actionID, List parameters) + { + RoomID = roomID; + ActionID = actionID; + Parameters = parameters; + } +} + +//[XmlRoot(ElementName = "ActionResponse")] +public class ActionResponse +{ + //[XmlElement(ElementName = "RequestID")] + public string RequestID { get; set; } + //[XmlElement(ElementName = "ActionID")] + public string ActionID { get; set; } + //[XmlElement(ElementName = "Parameters")] + public List Parameters { get; set; } +} + +//[XmlRoot(ElementName = "Parameter")] +public class Parameter +{ + //[XmlAttribute(AttributeName = "ID")] + public string ID { get; set; } + //[XmlAttribute(AttributeName = "Value")] + public string Value { get; set; } +} + +////[XmlRoot(ElementName = "Parameters")] +//public class Parameters +//{ +// //[XmlElement(ElementName = "Parameter")] +// public List Parameter { get; set; } +//} + +/// +/// Data structure for a ScheduleResponse from Fusion +/// +/// //[XmlRoot(ElementName = "ScheduleResponse")] +public class ScheduleResponse +{ + //[XmlElement(ElementName = "RequestID")] + public string RequestID { get; set; } + //[XmlElement(ElementName = "RoomID")] + public string RoomID { get; set; } + //[XmlElement(ElementName = "RoomName")] + public string RoomName { get; set; } + //[XmlElement("Event")] + public List Events { get; set; } + + public ScheduleResponse() + { + Events = new List(); + } +} + +//[XmlRoot(ElementName = "Event")] +public class Event +{ + //[XmlElement(ElementName = "MeetingID")] + public string MeetingID { get; set; } + //[XmlElement(ElementName = "RVMeetingID")] + public string RVMeetingID { get; set; } + //[XmlElement(ElementName = "Recurring")] + public string Recurring { get; set; } + //[XmlElement(ElementName = "InstanceID")] + public string InstanceID { get; set; } + //[XmlElement(ElementName = "dtStart")] + public DateTime dtStart { get; set; } + //[XmlElement(ElementName = "dtEnd")] + public DateTime dtEnd { get; set; } + //[XmlElement(ElementName = "Organizer")] + public string Organizer { get; set; } + //[XmlElement(ElementName = "Attendees")] + public Attendees Attendees { get; set; } + //[XmlElement(ElementName = "Resources")] + public Resources Resources { get; set; } + //[XmlElement(ElementName = "IsEvent")] + public string IsEvent { get; set; } + //[XmlElement(ElementName = "IsRoomViewMeeting")] + public string IsRoomViewMeeting { get; set; } + //[XmlElement(ElementName = "IsPrivate")] + public string IsPrivate { get; set; } + //[XmlElement(ElementName = "IsExchangePrivate")] + public string IsExchangePrivate { get; set; } + //[XmlElement(ElementName = "MeetingTypes")] + public MeetingTypes MeetingTypes { get; set; } + //[XmlElement(ElementName = "ParticipantCode")] + public string ParticipantCode { get; set; } + //[XmlElement(ElementName = "PhoneNo")] + public string PhoneNo { get; set; } + //[XmlElement(ElementName = "WelcomeMsg")] + public string WelcomeMsg { get; set; } + //[XmlElement(ElementName = "Subject")] + public string Subject { get; set; } + //[XmlElement(ElementName = "LiveMeeting")] + public LiveMeeting LiveMeeting { get; set; } + //[XmlElement(ElementName = "ShareDocPath")] + public string ShareDocPath { get; set; } + //[XmlElement(ElementName = "HaveAttendees")] + public string HaveAttendees { get; set; } + //[XmlElement(ElementName = "HaveResources")] + public string HaveResources { get; set; } /// - /// Represents a FusionAsset + /// Gets the duration of the meeting /// - public class FusionAsset + public string DurationInMinutes { - /// - /// Gets or sets the SlotNumber - /// - public uint SlotNumber { get; set; } - /// - /// Gets or sets the Name - /// - public string Name { get; set; } - /// - /// Gets or sets the Type - /// - public string Type { get; set; } - /// - /// Gets or sets the InstanceId - /// - public string InstanceId { get;set; } - - /// - /// Default constructor - /// - public FusionAsset() + get { + string duration; - } - - /// - /// FusionAsset constructor - /// - /// slot number of asset - /// name of the asset - /// type of the asset - /// instance ID of the asset - public FusionAsset(uint slotNum, string assetName, string type, string instanceId) - { - SlotNumber = slotNum; - Name = assetName; - Type = type; - if (string.IsNullOrEmpty(instanceId)) + var timeSpan = dtEnd.Subtract(dtStart); + int hours = timeSpan.Hours; + double minutes = timeSpan.Minutes; + double roundedMinutes = Math.Round(minutes); + if (hours > 0) { - InstanceId = Guid.NewGuid().ToString(); + duration = string.Format("{0} hours {1} minutes", hours, roundedMinutes); } else { - InstanceId = instanceId; + duration = string.Format("{0} minutes", roundedMinutes); } + + return duration; } } - //*************************************************************************************************** - /// - /// Represents a RoomSchedule + /// Gets the remaining time in the meeting. Returns null if the meeting is not currently in progress. /// - public class RoomSchedule + public string RemainingTime { - /// - /// Gets or sets the Meetings - /// - public List Meetings { get; set; } - - /// - /// RoomSchedule constructor - /// - public RoomSchedule() + get { - Meetings = new List(); - } - } + var now = DateTime.Now; - //**************************************************************************************************** - // Helper Classes for XML API + string remainingTime; - /// - /// Represents a LocalTimeRequest - /// - public class LocalTimeRequest - { - /// - /// Gets or sets the RequestID - /// - public string RequestID { get; set; } - } - - /// - /// All the data needed for a full schedule request in a room - /// - /// //[XmlRoot(ElementName = "RequestSchedule")] - public class RequestSchedule - { - //[XmlElement(ElementName = "RequestID")] - /// - /// Gets or sets the RequestID - /// - public string RequestID { get; set; } - - //[XmlElement(ElementName = "RoomID")] - /// - /// Gets or sets the RoomID - /// - public string RoomID { get; set; } - - //[XmlElement(ElementName = "Start")] - /// - /// Gets or sets the Start - /// - public DateTime Start { get; set; } - - //[XmlElement(ElementName = "HourSpan")] - /// - /// Gets or sets the HourSpan - /// - public double HourSpan { get; set; } - - /// - /// RequestSchedule constructor - /// - /// id of the request - /// id of the room - public RequestSchedule(string requestID, string roomID) - { - RequestID = requestID; - RoomID = roomID; - Start = DateTime.Now; - HourSpan = 24; - } - } - - - //[XmlRoot(ElementName = "RequestAction")] - /// - /// Represents a RequestAction - /// - public class RequestAction - { - //[XmlElement(ElementName = "RequestID")] - /// - /// Gets or sets the RequestID - /// - public string RequestID { get; set; } - //[XmlElement(ElementName = "RoomID")] - /// - /// Gets or sets the RoomID - /// - public string RoomID { get; set; } - //[XmlElement(ElementName = "ActionID")] - /// - /// Gets or sets the ActionID - /// - public string ActionID { get; set; } - //[XmlElement(ElementName = "Parameters")] - /// - /// Gets or sets the Parameters - /// - public List Parameters { get; set; } - - /// - /// RequestAction constructor - /// - /// id of the room - /// id of the action - /// list of parameters - public RequestAction(string roomID, string actionID, List parameters) - { - RoomID = roomID; - ActionID = actionID; - Parameters = parameters; - } - } - - //[XmlRoot(ElementName = "ActionResponse")] - /// - /// Represents a ActionResponse - /// - public class ActionResponse - { - //[XmlElement(ElementName = "RequestID")] - /// - /// Gets or sets the RequestID - /// - public string RequestID { get; set; } - - //[XmlElement(ElementName = "ActionID")] - /// - /// Gets or sets the ActionID - /// - public string ActionID { get; set; } - - //[XmlElement(ElementName = "Parameters")] - /// - /// Gets or sets the Parameters - /// - public List Parameters { get; set; } - } - - //[XmlRoot(ElementName = "Parameter")] - /// - /// Represents a Parameter - /// - public class Parameter - { - //[XmlAttribute(AttributeName = "ID")] - /// - /// Gets or sets the ID - /// - public string ID { get; set; } - - //[XmlAttribute(AttributeName = "Value")] - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - ////[XmlRoot(ElementName = "Parameters")] - //public class Parameters - //{ - // //[XmlElement(ElementName = "Parameter")] - // public List Parameter { get; set; } - //} - - /// - /// Data structure for a ScheduleResponse from Fusion - /// - /// //[XmlRoot(ElementName = "ScheduleResponse")] - public class ScheduleResponse - { - //[XmlElement(ElementName = "RequestID")] - /// - /// Gets or sets the RequestID - /// - public string RequestID { get; set; } - - //[XmlElement(ElementName = "RoomID")] - /// - /// Gets or sets the RoomID - /// - public string RoomID { get; set; } - - //[XmlElement(ElementName = "RoomName")] - /// - /// Gets or sets the RoomName - /// - public string RoomName { get; set; } - - //[XmlElement("Event")] - /// - /// Gets or sets the Events - /// - public List Events { get; set; } - - /// - /// ScheduleResponse constructor - /// - public ScheduleResponse() - { - Events = new List(); - } - } - - //[XmlRoot(ElementName = "Event")] - /// - /// Represents a Event - /// - public class Event - { - //[XmlElement(ElementName = "MeetingID")] - /// - /// Gets or sets the MeetingID - /// - public string MeetingID { get; set; } - //[XmlElement(ElementName = "RVMeetingID")] - /// - /// Gets or sets the RVMeetingID - /// - public string RVMeetingID { get; set; } - //[XmlElement(ElementName = "Recurring")] - /// - /// Gets or sets the Recurring - /// - public string Recurring { get; set; } - //[XmlElement(ElementName = "InstanceID")] - /// - /// Gets or sets the InstanceID - /// - public string InstanceID { get; set; } - //[XmlElement(ElementName = "dtStart")] - /// - /// Gets or sets the dtStart - /// - public DateTime dtStart { get; set; } - //[XmlElement(ElementName = "dtEnd")] - /// - /// Gets or sets the dtEnd - /// - public DateTime dtEnd { get; set; } - //[XmlElement(ElementName = "Organizer")] - /// - /// Gets or sets the Organizer - /// - public string Organizer { get; set; } - //[XmlElement(ElementName = "Attendees")] - /// - /// Gets or sets the Attendees - /// - public Attendees Attendees { get; set; } - //[XmlElement(ElementName = "Resources")] - /// - /// Gets or sets the Resources - /// - public Resources Resources { get; set; } - //[XmlElement(ElementName = "IsEvent")] - /// - /// Gets or sets the IsEvent - /// - public string IsEvent { get; set; } - //[XmlElement(ElementName = "IsRoomViewMeeting")] - /// - /// Gets or sets the IsRoomViewMeeting - /// - public string IsRoomViewMeeting { get; set; } - //[XmlElement(ElementName = "IsPrivate")] - /// - /// Gets or sets the IsPrivate - /// - public string IsPrivate { get; set; } - //[XmlElement(ElementName = "IsExchangePrivate")] - /// - /// Gets or sets the IsExchangePrivate - /// - public string IsExchangePrivate { get; set; } - //[XmlElement(ElementName = "MeetingTypes")] - /// - /// Gets or sets the MeetingTypes - /// - public MeetingTypes MeetingTypes { get; set; } - //[XmlElement(ElementName = "ParticipantCode")] - /// - /// Gets or sets the ParticipantCode - /// - public string ParticipantCode { get; set; } - //[XmlElement(ElementName = "PhoneNo")] - /// - /// Gets or sets the PhoneNo - /// - public string PhoneNo { get; set; } - //[XmlElement(ElementName = "WelcomeMsg")] - /// - /// Gets or sets the WelcomeMsg - /// - public string WelcomeMsg { get; set; } - //[XmlElement(ElementName = "Subject")] - /// - /// Gets or sets the Subject - /// - public string Subject { get; set; } - //[XmlElement(ElementName = "LiveMeeting")] - /// - /// Gets or sets the LiveMeeting - /// - public LiveMeeting LiveMeeting { get; set; } - //[XmlElement(ElementName = "ShareDocPath")] - /// - /// Gets or sets the ShareDocPath - /// - public string ShareDocPath { get; set; } - //[XmlElement(ElementName = "HaveAttendees")] - /// - /// Gets or sets the HaveAttendees - /// - public string HaveAttendees { get; set; } - //[XmlElement(ElementName = "HaveResources")] - /// - /// Gets or sets the HaveResources - /// - public string HaveResources { get; set; } - - /// - /// Gets the duration of the meeting - /// - public string DurationInMinutes - { - get + if (GetInProgress()) { - string duration; - - var timeSpan = dtEnd.Subtract(dtStart); + var timeSpan = dtEnd.Subtract(now); int hours = timeSpan.Hours; double minutes = timeSpan.Minutes; double roundedMinutes = Math.Round(minutes); if (hours > 0) { - duration = string.Format("{0} hours {1} minutes", hours, roundedMinutes); + remainingTime = string.Format("{0} hours {1} minutes", hours, roundedMinutes); } else { - duration = string.Format("{0} minutes", roundedMinutes); + remainingTime = string.Format("{0} minutes", roundedMinutes); } - return duration; - } - } - - /// - /// Gets the remaining time in the meeting. Returns null if the meeting is not currently in progress. - /// - public string RemainingTime - { - get - { - var now = DateTime.Now; - - string remainingTime; - - if (GetInProgress()) - { - var timeSpan = dtEnd.Subtract(now); - int hours = timeSpan.Hours; - double minutes = timeSpan.Minutes; - double roundedMinutes = Math.Round(minutes); - if (hours > 0) - { - remainingTime = string.Format("{0} hours {1} minutes", hours, roundedMinutes); - } - else - { - remainingTime = string.Format("{0} minutes", roundedMinutes); - } - - return remainingTime; - } - else - return null; - } - - } - - /// - /// Indicates that the meeting is in progress - /// - public bool isInProgress - { - get - { - return GetInProgress(); - } - } - - /// - /// Determines if the meeting is in progress - /// - /// Returns true if in progress - bool GetInProgress() - { - var now = DateTime.Now; - - if (now > dtStart && now < dtEnd) - { - return true; + return remainingTime; } else - return false; + return null; + } + + } + + /// + /// Indicates that the meeting is in progress + /// + public bool isInProgress + { + get + { + return GetInProgress(); } } - //[XmlRoot(ElementName = "Resources")] /// - /// Represents a Resources + /// Determines if the meeting is in progress /// - public class Resources + /// Returns true if in progress + bool GetInProgress() { - //[XmlElement(ElementName = "Rooms")] - /// - /// Gets or sets the Rooms - /// - public Rooms Rooms { get; set; } - } + var now = DateTime.Now; - //[XmlRoot(ElementName = "Rooms")] - /// - /// Represents a Rooms - /// - public class Rooms - { - //[XmlElement(ElementName = "Room")] - /// - /// Gets or sets the Room - /// - public List Room { get; set; } + if (now > dtStart && now < dtEnd) + { + return true; + } + else + return false; } +} - //[XmlRoot(ElementName = "Room")] - /// - /// Represents a Room - /// - public class Room - { - //[XmlElement(ElementName = "Name")] - /// - /// Gets or sets the Name - /// - public string Name { get; set; } - //[XmlElement(ElementName = "ID")] - /// - /// Gets or sets the ID - /// - public string ID { get; set; } - //[XmlElement(ElementName = "MPType")] - /// - /// Gets or sets the MPType - /// - public string MPType { get; set; } - } +//[XmlRoot(ElementName = "Resources")] +public class Resources +{ + //[XmlElement(ElementName = "Rooms")] + public Rooms Rooms { get; set; } +} - //[XmlRoot(ElementName = "Attendees")] - /// - /// Represents a Attendees - /// - public class Attendees - { - //[XmlElement(ElementName = "Required")] - /// - /// Gets or sets the Required - /// - public Required Required { get; set; } - //[XmlElement(ElementName = "Optional")] - /// - /// Gets or sets the Optional - /// - public Optional Optional { get; set; } - } +//[XmlRoot(ElementName = "Rooms")] +public class Rooms +{ + //[XmlElement(ElementName = "Room")] + public List Room { get; set; } +} - //[XmlRoot(ElementName = "Required")] - /// - /// Represents a Required - /// - public class Required - { - //[XmlElement(ElementName = "Attendee")] - /// - /// Gets or sets the Attendee - /// - public List Attendee { get; set; } - } +//[XmlRoot(ElementName = "Room")] +public class Room +{ + //[XmlElement(ElementName = "Name")] + public string Name { get; set; } + //[XmlElement(ElementName = "ID")] + public string ID { get; set; } + //[XmlElement(ElementName = "MPType")] + public string MPType { get; set; } +} - //[XmlRoot(ElementName = "Optional")] - /// - /// Represents a Optional - /// - public class Optional - { - //[XmlElement(ElementName = "Attendee")] - /// - /// Gets or sets the Attendee - /// - public List Attendee { get; set; } - } +//[XmlRoot(ElementName = "Attendees")] +public class Attendees +{ + //[XmlElement(ElementName = "Required")] + public Required Required { get; set; } + //[XmlElement(ElementName = "Optional")] + public Optional Optional { get; set; } +} - //[XmlRoot(ElementName = "MeetingType")] - /// - /// Represents a MeetingType - /// - public class MeetingType - { - //[XmlAttribute(AttributeName = "ID")] - /// - /// Gets or sets the ID - /// - public string ID { get; set; } - //[XmlAttribute(AttributeName = "Value")] - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } +//[XmlRoot(ElementName = "Required")] +public class Required +{ + //[XmlElement(ElementName = "Attendee")] + public List Attendee { get; set; } +} - //[XmlRoot(ElementName = "MeetingTypes")] - /// - /// Represents a MeetingTypes - /// - public class MeetingTypes - { - //[XmlElement(ElementName = "MeetingType")] - /// - /// Gets or sets the MeetingType - /// - public List MeetingType { get; set; } - } +//[XmlRoot(ElementName = "Optional")] +public class Optional +{ + //[XmlElement(ElementName = "Attendee")] + public List Attendee { get; set; } +} - //[XmlRoot(ElementName = "LiveMeeting")] - /// - /// Represents a LiveMeeting - /// - public class LiveMeeting - { - //[XmlElement(ElementName = "URL")] - /// - /// Gets or sets the URL - /// - public string URL { get; set; } - //[XmlElement(ElementName = "ID")] - /// - /// Gets or sets the ID - /// - public string ID { get; set; } - //[XmlElement(ElementName = "Key")] - /// - /// Gets or sets the Key - /// - public string Key { get; set; } - //[XmlElement(ElementName = "Subject")] - /// - /// Gets or sets the Subject - /// - public string Subject { get; set; } - } +//[XmlRoot(ElementName = "MeetingType")] +public class MeetingType +{ + //[XmlAttribute(AttributeName = "ID")] + public string ID { get; set; } + //[XmlAttribute(AttributeName = "Value")] + public string Value { get; set; } +} - //[XmlRoot(ElementName = "LiveMeetingURL")] - /// - /// Represents a LiveMeetingURL - /// - public class LiveMeetingURL - { - //[XmlElement(ElementName = "LiveMeeting")] - /// - /// Gets or sets the LiveMeeting - /// - public LiveMeeting LiveMeeting { get; set; } - } +//[XmlRoot(ElementName = "MeetingTypes")] +public class MeetingTypes +{ + //[XmlElement(ElementName = "MeetingType")] + public List MeetingType { get; set; } +} + +//[XmlRoot(ElementName = "LiveMeeting")] +public class LiveMeeting +{ + //[XmlElement(ElementName = "URL")] + public string URL { get; set; } + //[XmlElement(ElementName = "ID")] + public string ID { get; set; } + //[XmlElement(ElementName = "Key")] + public string Key { get; set; } + //[XmlElement(ElementName = "Subject")] + public string Subject { get; set; } +} + +//[XmlRoot(ElementName = "LiveMeetingURL")] +public class LiveMeetingURL +{ + //[XmlElement(ElementName = "LiveMeeting")] + public LiveMeeting LiveMeeting { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Global/EthernetAdapterInfo.cs b/src/PepperDash.Essentials.Core/Global/EthernetAdapterInfo.cs index 6565c6de..68701639 100644 --- a/src/PepperDash.Essentials.Core/Global/EthernetAdapterInfo.cs +++ b/src/PepperDash.Essentials.Core/Global/EthernetAdapterInfo.cs @@ -4,56 +4,56 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Represents a EthernetAdapterInfo +/// +public class EthernetAdapterInfo { /// - /// Represents a EthernetAdapterInfo + /// Gets or sets the Type /// - public class EthernetAdapterInfo - { - /// - /// Gets or sets the Type - /// - public EthernetAdapterType Type { get; set; } - /// - /// Gets or sets the DhcpIsOn - /// - public bool DhcpIsOn { get; set; } - /// - /// Gets or sets the Hostname - /// - public string Hostname { get; set; } - /// - /// Gets or sets the MacAddress - /// - public string MacAddress { get; set; } - /// - /// Gets or sets the IpAddress - /// - public string IpAddress { get; set; } - /// - /// Gets or sets the Subnet - /// - public string Subnet { get; set; } - /// - /// Gets or sets the Gateway - /// - public string Gateway { get; set; } - /// - /// Gets or sets the Dns1 - /// - public string Dns1 { get; set; } - /// - /// Gets or sets the Dns2 - /// - public string Dns2 { get; set; } - /// - /// Gets or sets the Dns3 - /// - public string Dns3 { get; set; } - /// - /// Gets or sets the Domain - /// - public string Domain { get; set; } - } -} \ No newline at end of file + public EthernetAdapterType Type { get; set; } + /// + /// Gets or sets the DhcpIsOn + /// + public bool DhcpIsOn { get; set; } + /// + /// Gets or sets the Hostname + /// + public string Hostname { get; set; } + /// + /// Gets or sets the MacAddress + /// + public string MacAddress { get; set; } + /// + /// Gets or sets the IpAddress + /// + public string IpAddress { get; set; } + /// + /// Gets or sets the Subnet + /// + public string Subnet { get; set; } + /// + /// Gets or sets the Gateway + /// + public string Gateway { get; set; } + /// + /// Gets or sets the Dns1 + /// + public string Dns1 { get; set; } + /// + /// Gets or sets the Dns2 + /// + public string Dns2 { get; set; } + /// + /// Gets or sets the Dns3 + /// + public string Dns3 { get; set; } + /// + /// Gets or sets the Domain + /// + public string Domain { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Global/Global.cs b/src/PepperDash.Essentials.Core/Global/Global.cs index d1ed3efb..5eeb5e96 100644 --- a/src/PepperDash.Essentials.Core/Global/Global.cs +++ b/src/PepperDash.Essentials.Core/Global/Global.cs @@ -4,7 +4,6 @@ using System.Text.RegularExpressions; using System.Globalization; using Crestron.SimplSharp; using System.Collections.Generic; -using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronDataStore; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DM; @@ -12,9 +11,6 @@ using Crestron.SimplSharpPro.DM; using PepperDash.Core; using PepperDash.Essentials.License; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Schema; using Serilog.Events; @@ -57,126 +53,126 @@ namespace PepperDash.Essentials.Core public static IFormatProvider Culture = CultureInfo.InvariantCulture; - /// - /// True when the processor type is a DMPS variant - /// - public static bool ControlSystemIsDmpsType + /// + /// True when the processor type is a DMPS variant + /// + public static bool ControlSystemIsDmpsType + { + get { - get + if(ControlSystem.SystemControl != null) { - if(ControlSystem.SystemControl != null) + if(ControlSystem.SystemControl.SystemControlType > 0) { - if(ControlSystem.SystemControl.SystemControlType > 0) - { - return true; - } - } - return false; + return true; + } } + return false; } + } - /// - /// True when the processor type is a DMPS 4K variant - /// - public static bool ControlSystemIsDmps4kType + /// + /// True when the processor type is a DMPS 4K variant + /// + public static bool ControlSystemIsDmps4kType + { + get { - get + if(ControlSystem.SystemControl != null) { - if(ControlSystem.SystemControl != null) + if(ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K150CSystemControl || + ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K200CSystemControl || + ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K250CSystemControl || + ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K300CSystemControl || + ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K350CSystemControl) { - if(ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K150CSystemControl || - ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K200CSystemControl || - ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K250CSystemControl || - ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K300CSystemControl || - ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K350CSystemControl) - { - return true; - } - } - return false; + return true; + } } + return false; } + } - /// - /// True when the processor type is a DMPS 4K 200/300/250/350 variant - /// - public static bool ControlSystemIsDmps4k3xxType + /// + /// True when the processor type is a DMPS 4K 200/300/250/350 variant + /// + public static bool ControlSystemIsDmps4k3xxType + { + get { - get + if(ControlSystem.SystemControl != null) { - if(ControlSystem.SystemControl != null) + if(ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K200CSystemControl || + ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K250CSystemControl || + ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K300CSystemControl || + ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K350CSystemControl) { - if(ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K200CSystemControl || - ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K250CSystemControl || - ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K300CSystemControl || - ControlSystem.SystemControl.SystemControlType == eSystemControlType.Dmps34K350CSystemControl) - { - return true; - } - } - return false; + return true; + } } + return false; } + } /// /// Gets or sets the FilePathPrefix /// public static string FilePathPrefix { get; private set; } - /// - /// The file path prefix to the applciation directory - /// - public static string ApplicationDirectoryPathPrefix + /// + /// The file path prefix to the applciation directory + /// + public static string ApplicationDirectoryPathPrefix + { + get { - get - { - return Crestron.SimplSharp.CrestronIO.Directory.GetApplicationDirectory(); - } + return Crestron.SimplSharp.CrestronIO.Directory.GetApplicationDirectory(); } + } - /// - /// Returns the directory separator character based on the running OS - /// - public static char DirectorySeparator + /// + /// Returns the directory separator character based on the running OS + /// + public static char DirectorySeparator + { + get { - get - { - return System.IO.Path.DirectorySeparatorChar; - } + return System.IO.Path.DirectorySeparatorChar; } + } - /// - /// Wildcarded config file name for global reference - /// - public const string ConfigFileName = "*configurationFile*.json"; + /// + /// Wildcarded config file name for global reference + /// + public const string ConfigFileName = "*configurationFile*.json"; - /// - /// Sets the file path prefix - /// - /// - public static void SetFilePathPrefix(string prefix) + /// + /// Sets the file path prefix + /// + /// + public static void SetFilePathPrefix(string prefix) + { + FilePathPrefix = prefix; + Debug.LogMessage(LogEventLevel.Information, "File Path Prefix set to '{0}'", FilePathPrefix); + } + + static string _AssemblyVersion; + + /// + /// Gets the Assembly Version of Essentials + /// + /// The Assembly Version at Runtime + public static string AssemblyVersion + { + get { - FilePathPrefix = prefix; - Debug.LogMessage(LogEventLevel.Information, "File Path Prefix set to '{0}'", FilePathPrefix); + return _AssemblyVersion; } - - static string _AssemblyVersion; - - /// - /// Gets the Assembly Version of Essentials - /// - /// The Assembly Version at Runtime - public static string AssemblyVersion + private set { - get - { - return _AssemblyVersion; - } - private set - { - _AssemblyVersion = value; - } + _AssemblyVersion = value; } + } /// /// Sets the Assembly version to the version of the Essentials Library @@ -201,18 +197,18 @@ namespace PepperDash.Essentials.Core { if (Regex.Match(AssemblyVersion, @"^(\d*).(\d*).(\d*).*").Groups[1].Value == "0") { - Debug.LogMessage(LogEventLevel.Verbose, "Running Local Build. Bypassing Dependency Check."); - return true; + Debug.LogMessage(LogEventLevel.Verbose, "Running Local Build. Bypassing Dependency Check."); + return true; } if (developmentVersions == null) { Debug.LogMessage(LogEventLevel.Information, - "Development Plugin does not specify a list of versions. Loading plugin may not work as expected. Checking Minumum version"); - return IsRunningMinimumVersionOrHigher(minimumVersion); + "Development Plugin does not specify a list of versions. Loading plugin may not work as expected. Checking Minumum version"); + return IsRunningMinimumVersionOrHigher(minimumVersion); } - Debug.LogMessage(LogEventLevel.Verbose, "Comparing running version '{0}' to minimum versions '{1}'", AssemblyVersion, developmentVersions); + Debug.LogMessage(LogEventLevel.Verbose, "Comparing running version '{0}' to minimum versions '{1}'", AssemblyVersion, developmentVersions); var versionMatch = developmentVersions.FirstOrDefault(x => x == AssemblyVersion); @@ -222,83 +218,83 @@ namespace PepperDash.Essentials.Core return false; } - Debug.LogMessage(LogEventLevel.Verbose, "Essentials Build {0} matches list of development builds", AssemblyVersion); + Debug.LogMessage(LogEventLevel.Verbose, "Essentials Build {0} matches list of development builds", AssemblyVersion); return IsRunningMinimumVersionOrHigher(minimumVersion); } - /// - /// Checks to see if the running version meets or exceed the minimum specified version. For beta versions (0.xx.yy), will always return true. - /// - /// Minimum specified version in format of xx.yy.zz - /// Returns true if the running version meets or exceeds the minimum specified version - public static bool IsRunningMinimumVersionOrHigher(string minimumVersion) + /// + /// Checks to see if the running version meets or exceed the minimum specified version. For beta versions (0.xx.yy), will always return true. + /// + /// Minimum specified version in format of xx.yy.zz + /// Returns true if the running version meets or exceeds the minimum specified version + public static bool IsRunningMinimumVersionOrHigher(string minimumVersion) + { + Debug.LogMessage(LogEventLevel.Verbose, "Comparing running version '{0}' to minimum version '{1}'", AssemblyVersion, minimumVersion); + + if (String.IsNullOrEmpty(minimumVersion)) { - Debug.LogMessage(LogEventLevel.Verbose, "Comparing running version '{0}' to minimum version '{1}'", AssemblyVersion, minimumVersion); - - if (String.IsNullOrEmpty(minimumVersion)) - { - Debug.LogMessage(LogEventLevel.Information,"Plugin does not specify a minimum version. Loading plugin may not work as expected. Proceeding with loading plugin"); - return true; - } - - var runtimeVersion = Regex.Match(AssemblyVersion, @"^(\d*).(\d*).(\d*).*"); - - var runtimeVersionMajor = Int16.Parse(runtimeVersion.Groups[1].Value); - var runtimeVersionMinor = Int16.Parse(runtimeVersion.Groups[2].Value); - var runtimeVersionBuild = Int16.Parse(runtimeVersion.Groups[3].Value); - - var runtimeVer = new Version(runtimeVersionMajor, runtimeVersionMinor, runtimeVersionBuild); - - Version minimumVer; - try - { - minimumVer = new Version(minimumVersion); - } - catch - { - Debug.LogMessage(LogEventLevel.Verbose, "unable to parse minimum version {0}. Bypassing plugin load.", minimumVersion); - return false; - } - - - // Check for beta build version - if (runtimeVer.Major != 0) - { - return runtimeVer.CompareTo(minimumVer) >= 0; - } - - Debug.LogMessage(LogEventLevel.Verbose, "Running Local Build. Bypassing Dependency Check."); + Debug.LogMessage(LogEventLevel.Information,"Plugin does not specify a minimum version. Loading plugin may not work as expected. Proceeding with loading plugin"); return true; - - /* - var minVersion = Regex.Match(minimumVersion, @"^(\d*).(\d*).(\d*)$"); - - if(!minVersion.Success) - { - - } - - var minVersionMajor = Int16.Parse(minVersion.Groups[1].Value); - var minVersionMinor = Int16.Parse(minVersion.Groups[2].Value); - var minVersionBuild = Int16.Parse(minVersion.Groups[3].Value); - - - - if (minVersionMajor > runtimeVersionMajor) - return false; - - if (minVersionMinor > runtimeVersionMinor) - return false; - - if (minVersionBuild > runtimeVersionBuild) - return false; - - return true; - */ } + + var runtimeVersion = Regex.Match(AssemblyVersion, @"^(\d*).(\d*).(\d*).*"); + + var runtimeVersionMajor = Int16.Parse(runtimeVersion.Groups[1].Value); + var runtimeVersionMinor = Int16.Parse(runtimeVersion.Groups[2].Value); + var runtimeVersionBuild = Int16.Parse(runtimeVersion.Groups[3].Value); + + var runtimeVer = new Version(runtimeVersionMajor, runtimeVersionMinor, runtimeVersionBuild); + + Version minimumVer; + try + { + minimumVer = new Version(minimumVersion); + } + catch + { + Debug.LogMessage(LogEventLevel.Verbose, "unable to parse minimum version {0}. Bypassing plugin load.", minimumVersion); + return false; + } + + + // Check for beta build version + if (runtimeVer.Major != 0) + { + return runtimeVer.CompareTo(minimumVer) >= 0; + } + + Debug.LogMessage(LogEventLevel.Verbose, "Running Local Build. Bypassing Dependency Check."); + return true; + + /* + var minVersion = Regex.Match(minimumVersion, @"^(\d*).(\d*).(\d*)$"); + + if(!minVersion.Success) + { + + } + + var minVersionMajor = Int16.Parse(minVersion.Groups[1].Value); + var minVersionMinor = Int16.Parse(minVersion.Groups[2].Value); + var minVersionBuild = Int16.Parse(minVersion.Groups[3].Value); + + + + if (minVersionMajor > runtimeVersionMajor) + return false; + + if (minVersionMinor > runtimeVersionMinor) + return false; + + if (minVersionBuild > runtimeVersionBuild) + return false; + + return true; + */ + } static Global() { @@ -310,17 +306,17 @@ namespace PepperDash.Essentials.Core return; } - try - { - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture("en"); - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture("en"); - } - catch (CultureNotFoundException) - { - // If specific culture fails, fall back to invariant - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; - } + try + { + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture("en"); + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture("en"); + } + catch (CultureNotFoundException) + { + // If specific culture fails, fall back to invariant + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; + } } } diff --git a/src/PepperDash.Essentials.Core/Global/JobTimer.cs b/src/PepperDash.Essentials.Core/Global/JobTimer.cs index 55814368..0d75a972 100644 --- a/src/PepperDash.Essentials.Core/Global/JobTimer.cs +++ b/src/PepperDash.Essentials.Core/Global/JobTimer.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using Crestron.SimplSharp; +using System.Timers; namespace PepperDash.Essentials.Core { @@ -11,7 +10,7 @@ namespace PepperDash.Essentials.Core /// public static class JobTimer { - static CTimer MinuteTimer; + static Timer MinuteTimer; static List Items = new List(); @@ -42,7 +41,9 @@ namespace PepperDash.Essentials.Core { if (Items.Count > 0 && MinuteTimer == null) { - MinuteTimer = new CTimer(o => MinuteTimerCallback(), null, 60000, 60000); + MinuteTimer = new Timer(60000); + MinuteTimer.Elapsed += (sender, e) => MinuteTimerCallback(); + MinuteTimer.Start(); } } diff --git a/src/PepperDash.Essentials.Core/Global/Scheduler.cs b/src/PepperDash.Essentials.Core/Global/Scheduler.cs index c946b67d..ff1773fe 100644 --- a/src/PepperDash.Essentials.Core/Global/Scheduler.cs +++ b/src/PepperDash.Essentials.Core/Global/Scheduler.cs @@ -1,103 +1,100 @@ using System; using System.Collections.Generic; -using System.Linq; using Crestron.SimplSharp; using Crestron.SimplSharp.Scheduler; using PepperDash.Core; -using PepperDash.Essentials.Core.Fusion; using PepperDash.Essentials.Room.Config; using Serilog.Events; -using Activator = System.Activator; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Global Scheduler for the system +/// +public static class Scheduler { - /// - /// Global Scheduler for the system - /// - public static class Scheduler + private static readonly Dictionary EventGroups = new Dictionary(); + + static Scheduler() { - private static readonly Dictionary EventGroups = new Dictionary(); + CrestronConsole.AddNewConsoleCommand(DeleteEventGroup, "DeleteEventGroup", "Deletes the event group by key", ConsoleAccessLevelEnum.AccessOperator); - static Scheduler() + CrestronConsole.AddNewConsoleCommand(ClearEventsFromGroup, "ClearAllEvents", "Clears all scheduled events for this group", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(ListAllEventGroups, "ListAllEventGroups", "Lists all the event groups by key", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(ListAllEventsForGroup, "ListEventsForGroup", + "Lists all events for the given group", ConsoleAccessLevelEnum.AccessOperator); + } + + + static void DeleteEventGroup(string groupName) + { + if (EventGroups.ContainsKey(groupName)) { - CrestronConsole.AddNewConsoleCommand(DeleteEventGroup, "DeleteEventGroup", "Deletes the event group by key", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(ClearEventsFromGroup, "ClearAllEvents", "Clears all scheduled events for this group", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(ListAllEventGroups, "ListAllEventGroups", "Lists all the event groups by key", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(ListAllEventsForGroup, "ListEventsForGroup", - "Lists all events for the given group", ConsoleAccessLevelEnum.AccessOperator); - } - - - static void DeleteEventGroup(string groupName) - { - if (EventGroups.ContainsKey(groupName)) - { - var group = EventGroups[groupName]; - - EventGroups.Remove(groupName); - - group.Dispose(); - - group = null; - } - } - - /// - /// Clears (deletes) all events from a group - /// - /// - static void ClearEventsFromGroup(string groupName) - { - if (!EventGroups.ContainsKey(groupName)) - { - Debug.LogMessage(LogEventLevel.Information, - "[Scheduler]: Unable to delete events from group '{0}'. Group not found in EventGroups dictionary.", null, - groupName); - return; - } - var group = EventGroups[groupName]; - if (group != null) - { - group.ClearAllEvents(); + EventGroups.Remove(groupName); - Debug.LogMessage(LogEventLevel.Information, "[Scheduler]: All events deleted from group '{0}'", null, groupName); - } - else - Debug.LogMessage(LogEventLevel.Information, - "[Scheduler]: Unable to delete events from group '{0}'. Group not found in EventGroups dictionary.", null, - groupName); + group.Dispose(); + + group = null; + } + } + + /// + /// Clears (deletes) all events from a group + /// + /// + static void ClearEventsFromGroup(string groupName) + { + if (!EventGroups.ContainsKey(groupName)) + { + Debug.LogMessage(LogEventLevel.Information, + "[Scheduler]: Unable to delete events from group '{0}'. Group not found in EventGroups dictionary.", null, + groupName); + return; } - static void ListAllEventGroups(string command) + var group = EventGroups[groupName]; + + if (group != null) { - CrestronConsole.ConsoleCommandResponse("Event Groups:"); - foreach (var group in EventGroups) - { - CrestronConsole.ConsoleCommandResponse($"{group.Key}"); - } + group.ClearAllEvents(); + + Debug.LogMessage(LogEventLevel.Information, "[Scheduler]: All events deleted from group '{0}'", null, groupName); + } + else + Debug.LogMessage(LogEventLevel.Information, + "[Scheduler]: Unable to delete events from group '{0}'. Group not found in EventGroups dictionary.", null, + groupName); + } + + static void ListAllEventGroups(string command) + { + CrestronConsole.ConsoleCommandResponse("Event Groups:"); + foreach (var group in EventGroups) + { + CrestronConsole.ConsoleCommandResponse($"{group.Key}"); + } + } + + static void ListAllEventsForGroup(string args) + { + Debug.LogMessage(LogEventLevel.Information, "Getting events for group {0}...", null, args); + + ScheduledEventGroup group; + + if (!EventGroups.TryGetValue(args, out group)) + { + Debug.LogMessage(LogEventLevel.Information, "Unabled to get event group for key {0}", null, args); + return; } - static void ListAllEventsForGroup(string args) + foreach (var evt in group.ScheduledEvents) { - Debug.LogMessage(LogEventLevel.Information, "Getting events for group {0}...", null, args); - - ScheduledEventGroup group; - - if (!EventGroups.TryGetValue(args, out group)) - { - Debug.LogMessage(LogEventLevel.Information, "Unabled to get event group for key {0}", null, args); - return; - } - - foreach (var evt in group.ScheduledEvents) - { - CrestronConsole.ConsoleCommandResponse( + CrestronConsole.ConsoleCommandResponse( $@" ****Event key {evt.Key}**** Event state: {evt.Value.EventState} @@ -107,224 +104,223 @@ Acknowlegable: {evt.Value.Acknowledgeable} Recurrence: {evt.Value.Recurrence.Recurrence} Recurrence Days: {evt.Value.Recurrence.RecurrenceDays} ********************"); - } - } - - /// - /// Adds the event group to the global list - /// - /// - /// - /// AddEventGroup method - /// - public static void AddEventGroup(ScheduledEventGroup eventGroup) - { - // Add this group to the global collection - if (!EventGroups.ContainsKey(eventGroup.Name)) - EventGroups.Add(eventGroup.Name, eventGroup); - } - - /// - /// Removes the event group from the global list - /// - /// - /// - /// RemoveEventGroup method - /// - public static void RemoveEventGroup(ScheduledEventGroup eventGroup) - { - if(!EventGroups.ContainsKey(eventGroup.Name)) - EventGroups.Remove(eventGroup.Name); - } - - /// - /// Gets the event group by key - /// - /// key of the event group - /// - public static ScheduledEventGroup GetEventGroup(string key) - { - ScheduledEventGroup returnValue; - - return EventGroups.TryGetValue(key, out returnValue) ? returnValue : null; } } /// - /// SchedulerUtilities class + /// Adds the event group to the global list /// - public static class SchedulerUtilities + /// + /// + /// AddEventGroup method + /// + public static void AddEventGroup(ScheduledEventGroup eventGroup) { - /// - /// Checks the day of week in eventTime to see if it matches the weekdays defined in the recurrence enum. - /// - /// - /// - /// - /// - /// CheckIfDayOfWeekMatchesRecurrenceDays method - /// - public static bool CheckIfDayOfWeekMatchesRecurrenceDays(DateTime eventTime, ScheduledEventCommon.eWeekDays recurrence) + // Add this group to the global collection + if (!EventGroups.ContainsKey(eventGroup.Name)) + EventGroups.Add(eventGroup.Name, eventGroup); + } + + /// + /// Removes the event group from the global list + /// + /// + /// + /// RemoveEventGroup method + /// + public static void RemoveEventGroup(ScheduledEventGroup eventGroup) + { + if (!EventGroups.ContainsKey(eventGroup.Name)) + EventGroups.Remove(eventGroup.Name); + } + + /// + /// Gets the event group by key + /// + /// key of the event group + /// + public static ScheduledEventGroup GetEventGroup(string key) + { + ScheduledEventGroup returnValue; + + return EventGroups.TryGetValue(key, out returnValue) ? returnValue : null; + } +} + +/// +/// SchedulerUtilities class +/// +public static class SchedulerUtilities +{ + /// + /// Checks the day of week in eventTime to see if it matches the weekdays defined in the recurrence enum. + /// + /// + /// + /// + /// + /// CheckIfDayOfWeekMatchesRecurrenceDays method + /// + public static bool CheckIfDayOfWeekMatchesRecurrenceDays(DateTime eventTime, ScheduledEventCommon.eWeekDays recurrence) + { + bool isMatch = false; + + var dayOfWeek = eventTime.DayOfWeek; + + Debug.LogMessage(LogEventLevel.Debug, "[Scheduler]: eventTime day of week is: {0}", null, dayOfWeek); + switch (dayOfWeek) { - bool isMatch = false; - - var dayOfWeek = eventTime.DayOfWeek; - - Debug.LogMessage(LogEventLevel.Debug, "[Scheduler]: eventTime day of week is: {0}",null, dayOfWeek); - switch (dayOfWeek) - { - case DayOfWeek.Sunday: - { - if ((recurrence & ScheduledEventCommon.eWeekDays.Sunday) == ScheduledEventCommon.eWeekDays.Sunday) - isMatch = true; - break; - } - case DayOfWeek.Monday: - { - if ((recurrence & ScheduledEventCommon.eWeekDays.Monday) == ScheduledEventCommon.eWeekDays.Monday) - isMatch = true; - break; - } - case DayOfWeek.Tuesday: - { - if ((recurrence & ScheduledEventCommon.eWeekDays.Tuesday) == ScheduledEventCommon.eWeekDays.Tuesday) - isMatch = true; - break; - } - case DayOfWeek.Wednesday: - { - if ((recurrence & ScheduledEventCommon.eWeekDays.Wednesday) == ScheduledEventCommon.eWeekDays.Wednesday) - isMatch = true; - break; - } - case DayOfWeek.Thursday: - { - if ((recurrence & ScheduledEventCommon.eWeekDays.Thursday) == ScheduledEventCommon.eWeekDays.Thursday) - isMatch = true; - break; - } - case DayOfWeek.Friday: - { - if ((recurrence & ScheduledEventCommon.eWeekDays.Friday) == ScheduledEventCommon.eWeekDays.Friday) - isMatch = true; - break; - } - case DayOfWeek.Saturday: - { - if ((recurrence & ScheduledEventCommon.eWeekDays.Saturday) == ScheduledEventCommon.eWeekDays.Saturday) - isMatch = true; - break; - } - } - - Debug.LogMessage(LogEventLevel.Debug, "[Scheduler]: eventTime day of week matches recurrence days: {0}", isMatch); - - return isMatch; + case DayOfWeek.Sunday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Sunday) == ScheduledEventCommon.eWeekDays.Sunday) + isMatch = true; + break; + } + case DayOfWeek.Monday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Monday) == ScheduledEventCommon.eWeekDays.Monday) + isMatch = true; + break; + } + case DayOfWeek.Tuesday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Tuesday) == ScheduledEventCommon.eWeekDays.Tuesday) + isMatch = true; + break; + } + case DayOfWeek.Wednesday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Wednesday) == ScheduledEventCommon.eWeekDays.Wednesday) + isMatch = true; + break; + } + case DayOfWeek.Thursday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Thursday) == ScheduledEventCommon.eWeekDays.Thursday) + isMatch = true; + break; + } + case DayOfWeek.Friday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Friday) == ScheduledEventCommon.eWeekDays.Friday) + isMatch = true; + break; + } + case DayOfWeek.Saturday: + { + if ((recurrence & ScheduledEventCommon.eWeekDays.Saturday) == ScheduledEventCommon.eWeekDays.Saturday) + isMatch = true; + break; + } } + Debug.LogMessage(LogEventLevel.Debug, "[Scheduler]: eventTime day of week matches recurrence days: {0}", isMatch); - /// - /// CheckEventTimeForMatch method - /// - public static bool CheckEventTimeForMatch(ScheduledEvent evnt, DateTime time) - { - return evnt.DateAndTime.Hour == time.Hour && evnt.DateAndTime.Minute == time.Minute; - } + return isMatch; + } - /// - /// CheckEventRecurrenceForMatch method - /// - public static bool CheckEventRecurrenceForMatch(ScheduledEvent evnt, ScheduledEventCommon.eWeekDays days) - { - return evnt.Recurrence.RecurrenceDays == days; - } - /// - /// CreateEventFromConfig method - /// - public static void CreateEventFromConfig(ScheduledEventConfig config, ScheduledEventGroup group, ScheduledEvent.UserEventCallBack handler) + /// + /// CheckEventTimeForMatch method + /// + public static bool CheckEventTimeForMatch(ScheduledEvent evnt, DateTime time) + { + return evnt.DateAndTime.Hour == time.Hour && evnt.DateAndTime.Minute == time.Minute; + } + + /// + /// CheckEventRecurrenceForMatch method + /// + public static bool CheckEventRecurrenceForMatch(ScheduledEvent evnt, ScheduledEventCommon.eWeekDays days) + { + return evnt.Recurrence.RecurrenceDays == days; + } + + /// + /// CreateEventFromConfig method + /// + public static void CreateEventFromConfig(ScheduledEventConfig config, ScheduledEventGroup group, ScheduledEvent.UserEventCallBack handler) + { + try { - try + if (group == null) { - if (group == null) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to create event. Group is null", null, null); - return; - } - var scheduledEvent = new ScheduledEvent(config.Key, group) - { - Acknowledgeable = config.Acknowledgeable, - Persistent = config.Persistent - }; - - scheduledEvent.UserCallBack += handler; - - scheduledEvent.DateAndTime.SetFirstDayOfWeek(ScheduledEventCommon.eFirstDayOfWeek.Sunday); - - var eventTime = DateTime.Parse(config.Time); - - if (DateTime.Now > eventTime) - { - eventTime = eventTime.AddDays(1); - } - - Debug.LogMessage(LogEventLevel.Verbose, "[Scheduler] Current Date day of week: {0} recurrence days: {1}", null, eventTime.DayOfWeek, - config.Days); - - var dayOfWeekConverted = ConvertDayOfWeek(eventTime); - - Debug.LogMessage(LogEventLevel.Debug, "[Scheduler] eventTime Day: {0}", null, dayOfWeekConverted); - - while (!dayOfWeekConverted.IsFlagSet(config.Days)) - { - eventTime = eventTime.AddDays(1); - - dayOfWeekConverted = ConvertDayOfWeek(eventTime); - } - - scheduledEvent.DateAndTime.SetAbsoluteEventTime(eventTime); - - scheduledEvent.Recurrence.Weekly(config.Days); - - Debug.LogMessage(LogEventLevel.Verbose, $"[Scheduler] Event State: {scheduledEvent.EventState}", null, null); - - if (config.Enable && scheduledEvent.EventState != ScheduledEventCommon.eEventState.Enabled) - { - scheduledEvent.Enable(); - } - else if (!config.Enable && scheduledEvent.EventState != ScheduledEventCommon.eEventState.Disabled) - { - scheduledEvent.Disable(); - } - + Debug.LogMessage(LogEventLevel.Information, "Unable to create event. Group is null", null, null); + return; } - catch (Exception e) + var scheduledEvent = new ScheduledEvent(config.Key, group) { + Acknowledgeable = config.Acknowledgeable, + Persistent = config.Persistent + }; - Debug.LogMessage(LogEventLevel.Error, "Error creating scheduled event: {0}", null, e); + scheduledEvent.UserCallBack += handler; + + scheduledEvent.DateAndTime.SetFirstDayOfWeek(ScheduledEventCommon.eFirstDayOfWeek.Sunday); + + var eventTime = DateTime.Parse(config.Time); + + if (DateTime.Now > eventTime) + { + eventTime = eventTime.AddDays(1); } + + Debug.LogMessage(LogEventLevel.Verbose, "[Scheduler] Current Date day of week: {0} recurrence days: {1}", null, eventTime.DayOfWeek, + config.Days); + + var dayOfWeekConverted = ConvertDayOfWeek(eventTime); + + Debug.LogMessage(LogEventLevel.Debug, "[Scheduler] eventTime Day: {0}", null, dayOfWeekConverted); + + while (!dayOfWeekConverted.IsFlagSet(config.Days)) + { + eventTime = eventTime.AddDays(1); + + dayOfWeekConverted = ConvertDayOfWeek(eventTime); + } + + scheduledEvent.DateAndTime.SetAbsoluteEventTime(eventTime); + + scheduledEvent.Recurrence.Weekly(config.Days); + + Debug.LogMessage(LogEventLevel.Verbose, $"[Scheduler] Event State: {scheduledEvent.EventState}", null, null); + + if (config.Enable && scheduledEvent.EventState != ScheduledEventCommon.eEventState.Enabled) + { + scheduledEvent.Enable(); + } + else if (!config.Enable && scheduledEvent.EventState != ScheduledEventCommon.eEventState.Disabled) + { + scheduledEvent.Disable(); + } + } - - private static ScheduledEventCommon.eWeekDays ConvertDayOfWeek(DateTime eventTime) + catch (Exception e) { - return (ScheduledEventCommon.eWeekDays) Enum.Parse(typeof(ScheduledEventCommon.eWeekDays), eventTime.DayOfWeek.ToString(), true); - } - private static bool IsFlagSet(this T value, T flag) where T : struct - { - CheckIsEnum(true); - - var lValue = Convert.ToInt64(value); - var lFlag = Convert.ToInt64(flag); - - return (lValue & lFlag) != 0; - } - - private static void CheckIsEnum(bool withFlags) - { - if (!typeof(T).IsEnum) - throw new ArgumentException(string.Format("Type '{0}' is not an enum", typeof(T).FullName)); - if (withFlags && !Attribute.IsDefined(typeof(T), typeof(FlagsAttribute))) - throw new ArgumentException(string.Format("Type '{0}' doesn't have the 'Flags' attribute", typeof(T).FullName)); + Debug.LogMessage(LogEventLevel.Error, "Error creating scheduled event: {0}", null, e); } } -} \ No newline at end of file + + private static ScheduledEventCommon.eWeekDays ConvertDayOfWeek(DateTime eventTime) + { + return (ScheduledEventCommon.eWeekDays)Enum.Parse(typeof(ScheduledEventCommon.eWeekDays), eventTime.DayOfWeek.ToString(), true); + } + + private static bool IsFlagSet(this T value, T flag) where T : struct + { + CheckIsEnum(true); + + var lValue = Convert.ToInt64(value); + var lFlag = Convert.ToInt64(flag); + + return (lValue & lFlag) != 0; + } + + private static void CheckIsEnum(bool withFlags) + { + if (!typeof(T).IsEnum) + throw new ArgumentException(string.Format("Type '{0}' is not an enum", typeof(T).FullName)); + if (withFlags && !Attribute.IsDefined(typeof(T), typeof(FlagsAttribute))) + throw new ArgumentException(string.Format("Type '{0}' doesn't have the 'Flags' attribute", typeof(T).FullName)); + } +} diff --git a/src/PepperDash.Essentials.Core/InUseTracking/IInUseTracking.cs b/src/PepperDash.Essentials.Core/InUseTracking/IInUseTracking.cs index 59ea87b3..440b14bd 100644 --- a/src/PepperDash.Essentials.Core/InUseTracking/IInUseTracking.cs +++ b/src/PepperDash.Essentials.Core/InUseTracking/IInUseTracking.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Defines a class that uses an InUseTracker /// @@ -15,5 +15,4 @@ namespace PepperDash.Essentials.Core /// Gets the InUseTracker /// InUseTracking InUseTracker { get; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/InUseTracking/InUseTracking.cs b/src/PepperDash.Essentials.Core/InUseTracking/InUseTracking.cs index c0fcdbf6..247e8548 100644 --- a/src/PepperDash.Essentials.Core/InUseTracking/InUseTracking.cs +++ b/src/PepperDash.Essentials.Core/InUseTracking/InUseTracking.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Provides in use tracking. Objects can register with this. InUseFeedback can provide /// events when usage changes. @@ -113,5 +113,4 @@ namespace PepperDash.Essentials.Core // Tracker = tracker; // EventType = eventType; // } - //} -} \ No newline at end of file + //} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs b/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs index d55194fd..78446c6f 100644 --- a/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs +++ b/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs @@ -5,16 +5,15 @@ using System.Text; using Crestron.SimplSharp; using PepperDash.Core; -namespace PepperDash.Essentials.Core.Interfaces +namespace PepperDash.Essentials.Core.Interfaces; + +/// +/// Defines the contract for ILogStrings +/// +public interface ILogStrings : IKeyed { - /// - /// Defines the contract for ILogStrings - /// - public interface ILogStrings : IKeyed - { - /// - /// Defines a class that is capable of logging a string - /// - void SendToLog(IKeyed device, string logMessage); - } + /// + /// Defines a class that is capable of logging a string + /// + void SendToLog(IKeyed device, string logMessage); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs b/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs index bb835919..b9d61df1 100644 --- a/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs +++ b/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs @@ -5,17 +5,15 @@ using System.Text; using Crestron.SimplSharp; using PepperDash.Core; -namespace PepperDash.Essentials.Core.Interfaces -{ - /// - /// Defines the contract for ILogStringsWithLevel - /// - public interface ILogStringsWithLevel : IKeyed - { - /// - /// Defines a class that is capable of logging a string with an int level - /// - void SendToLog(IKeyed device, Debug.ErrorLogLevel level,string logMessage); - } +namespace PepperDash.Essentials.Core.Interfaces; +/// +/// Defines the contract for ILogStringsWithLevel +/// +public interface ILogStringsWithLevel : IKeyed +{ + /// + /// Defines a class that is capable of logging a string with an int level + /// + void SendToLog(IKeyed device, Debug.ErrorLogLevel level, string logMessage); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/JoinMaps/JoinMapBase.cs b/src/PepperDash.Essentials.Core/JoinMaps/JoinMapBase.cs index d2e7d036..9cd6776e 100644 --- a/src/PepperDash.Essentials.Core/JoinMaps/JoinMapBase.cs +++ b/src/PepperDash.Essentials.Core/JoinMaps/JoinMapBase.cs @@ -15,648 +15,526 @@ using PepperDash.Essentials.Core.Config; using Newtonsoft.Json; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public static class JoinMapHelper { /// - /// JoinMapHelper class + /// Attempts to get the serialized join map from config /// - public static class JoinMapHelper + /// + /// + public static string GetSerializedJoinMapForDevice(string joinMapKey) { - /// - /// Attempts to get the serialized join map from config - /// - /// - /// - /// - /// GetSerializedJoinMapForDevice method - /// - public static string GetSerializedJoinMapForDevice(string joinMapKey) + if (string.IsNullOrEmpty(joinMapKey)) + return null; + + var joinMap = ConfigReader.ConfigObject.JoinMaps[joinMapKey]; + + return joinMap.ToString(); + } + + /// + /// Attempts to get the serialized join map from config + /// + /// + /// + public static string GetJoinMapForDevice(string joinMapKey) + { + return GetSerializedJoinMapForDevice(joinMapKey); + } + + /// + /// Attempts to find a custom join map by key and returns it deserialized if found + /// + /// + /// + public static Dictionary TryGetJoinMapAdvancedForDevice(string joinMapKey) + { + try { if (string.IsNullOrEmpty(joinMapKey)) return null; - var joinMap = ConfigReader.ConfigObject.JoinMaps[joinMapKey]; - - return joinMap.ToString(); - } - - /// - /// Attempts to get the serialized join map from config - /// - /// - /// - /// - /// GetJoinMapForDevice method - /// - public static string GetJoinMapForDevice(string joinMapKey) - { - return GetSerializedJoinMapForDevice(joinMapKey); - } - - /// - /// Attempts to find a custom join map by key and returns it deserialized if found - /// - /// - /// - public static Dictionary TryGetJoinMapAdvancedForDevice(string joinMapKey) - { - try + if (!ConfigReader.ConfigObject.JoinMaps.ContainsKey(joinMapKey)) { - if (string.IsNullOrEmpty(joinMapKey)) - return null; - - if (!ConfigReader.ConfigObject.JoinMaps.ContainsKey(joinMapKey)) - { - Debug.LogMessage(LogEventLevel.Verbose, "No Join Map found in config with key: '{0}'", joinMapKey); - return null; - } - - Debug.LogMessage(LogEventLevel.Verbose, "Attempting to load custom join map with key: {0}", joinMapKey); - - var joinMapJToken = ConfigReader.ConfigObject.JoinMaps[joinMapKey]; - - if (joinMapJToken == null) - return null; - - var joinMapData = joinMapJToken.ToObject>(); - - return joinMapData; - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Verbose, "Error getting join map for key: '{0}'. Error: {1}", joinMapKey, e); + Debug.LogMessage(LogEventLevel.Verbose, "No Join Map found in config with key: '{0}'", joinMapKey); return null; } - } + Debug.LogMessage(LogEventLevel.Verbose, "Attempting to load custom join map with key: {0}", joinMapKey); + + var joinMapJToken = ConfigReader.ConfigObject.JoinMaps[joinMapKey]; + + if (joinMapJToken == null) + return null; + + var joinMapData = joinMapJToken.ToObject>(); + + return joinMapData; + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Verbose, "Error getting join map for key: '{0}'. Error: {1}", joinMapKey, e); + return null; + } } +} + +/// +/// Base class for join maps +/// +public abstract class JoinMapBaseAdvanced +{ + protected uint JoinOffset; + /// - /// Base class for join maps + /// The collection of joins and associated metadata /// - public abstract class JoinMapBaseAdvanced + public Dictionary Joins { get; private set; } + + protected JoinMapBaseAdvanced(uint joinStart) { - /// - /// The join offset - /// - protected uint JoinOffset; + Joins = new Dictionary(); - /// - /// The collection of joins and associated metadata - /// - public Dictionary Joins { get; private set; } + JoinOffset = joinStart - 1; + } - /// - /// Constructor - /// - /// join start offset - protected JoinMapBaseAdvanced(uint joinStart) + protected JoinMapBaseAdvanced(uint joinStart, Type type):this(joinStart) + { + AddJoins(type); + } + + protected void AddJoins(Type type) + { + var fields = + type.GetFields(BindingFlags.Public | BindingFlags.Instance) + .Where(f => f.IsDefined(typeof (JoinNameAttribute), true)).ToList(); + + Debug.LogMessage(LogEventLevel.Debug, "Got {fields} with JoinNameAttribute", fields.Count); + + foreach (var field in fields) { - Joins = new Dictionary(); + var childClass = Convert.ChangeType(this, type, null); - JoinOffset = joinStart - 1; - } + //this here is JoinMapBaseAdvanced, not the child class. JoinMapBaseAdvanced has no fields. - /// - /// Constructor - /// - /// join start offset - /// type of joinstart - protected JoinMapBaseAdvanced(uint joinStart, Type type):this(joinStart) - { - AddJoins(type); - } - - /// - /// AddJoins method - /// - /// type of join to add - protected void AddJoins(Type type) - { - var fields = - type.GetFields(BindingFlags.Public | BindingFlags.Instance) - .Where(f => f.IsDefined(typeof (JoinNameAttribute), true)).ToList(); - - Debug.LogMessage(LogEventLevel.Debug, "Got {fields} with JoinNameAttribute", fields.Count); - - foreach (var field in fields) + if (!(field.GetValue(childClass) is JoinDataComplete value)) { - var childClass = Convert.ChangeType(this, type, null); - - //this here is JoinMapBaseAdvanced, not the child class. JoinMapBaseAdvanced has no fields. - - if (!(field.GetValue(childClass) is JoinDataComplete value)) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to cast base class to {0}", type.Name); - continue; - } - - value.SetJoinOffset(JoinOffset); - - var joinName = value.GetNameAttribute(field); - - if (string.IsNullOrEmpty(joinName)) continue; - - Joins.Add(joinName, value); + Debug.LogMessage(LogEventLevel.Information, "Unable to cast base class to {0}", type.Name); + continue; } + value.SetJoinOffset(JoinOffset); - if (Debug.Level > 0) - { - PrintJoinMapInfo(); - } - } + var joinName = value.GetNameAttribute(field); - /// - /// PrintJoinMapInfo method - /// - public void PrintJoinMapInfo() - { - var sb = JoinmapStringBuilder(); + if (string.IsNullOrEmpty(joinName)) continue; - CrestronConsole.ConsoleCommandResponse(sb.ToString()); - } - - private StringBuilder JoinmapStringBuilder() - { - var sb = new StringBuilder(); - - var lineEnding = "\r\n"; - - var digitals = - Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Digital)) - .ToDictionary(j => j.Key, j => j.Value); - - var analogs = Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Analog)) - .ToDictionary(j => j.Key, j => j.Value); - - var serials = - Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Serial)) - .ToDictionary(j => j.Key, j => j.Value); - - Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Digital join count {digitalCount} Analog join count {analogCount} Serial join count {serialCount}", null, digitals.Count, analogs.Count, serials.Count); - - // Get the joins of each type and print them - sb.Append($"# {GetType().Name}\r\n"); - sb.Append(lineEnding); - sb.Append($"## Digitals{lineEnding}"); - sb.Append(lineEnding); - // Get the joins of each type and print them - - var digitalSb = AppendJoinList(GetSortedJoins(digitals)); - digitalSb.Append($"## Analogs{lineEnding}"); - digitalSb.Append(lineEnding); - - var analogSb = AppendJoinList(GetSortedJoins(analogs)); - analogSb.Append($"## Serials{lineEnding}"); - analogSb.Append(lineEnding); - - - var serialSb = AppendJoinList(GetSortedJoins(serials)); - - sb.EnsureCapacity(sb.Length + digitalSb.Length + analogSb.Length + serialSb.Length); - sb.Append(digitalSb).Append(analogSb).Append(serialSb); - return sb; - } - - /// - /// MarkdownJoinMapInfo method - /// - public void MarkdownJoinMapInfo(string deviceKey, string bridgeKey) - { - var pluginType = GetType().Name; - - CrestronConsole.ConsoleCommandResponse("{0}:\n", pluginType); - - - - WriteJoinmapMarkdown(JoinmapStringBuilder(), pluginType, bridgeKey, deviceKey); - - } - - private static void WriteJoinmapMarkdown(StringBuilder stringBuilder, string pluginType, string bridgeKey, string deviceKey) - { - var fileName = string.Format("{0}{1}{2}__{3}__{4}.md", Global.FilePathPrefix, "joinMaps/", pluginType, bridgeKey, deviceKey); - - using (var sw = new StreamWriter(fileName)) - { - sw.WriteLine(stringBuilder.ToString()); - CrestronConsole.ConsoleCommandResponse("Joinmap Readme generated and written to {0}", fileName); - } - - } - - /// - /// Returns a sorted list by JoinNumber - /// - /// - /// - static List> GetSortedJoins(Dictionary joins) - { - var sortedJoins = joins.ToList(); - - sortedJoins.Sort((pair1, pair2) => pair1.Value.JoinNumber.CompareTo(pair2.Value.JoinNumber)); - - return sortedJoins; + Joins.Add(joinName, value); } - static StringBuilder AppendJoinList(List> joins) + if (Debug.Level > 0) { - var sb = new StringBuilder(); - const string stringFormatter = "| {0} | {1} | {2} | {3} | {4} |\r\n"; - const int joinNumberLen = 11; - const int joinSpanLen = 9; - const int typeLen = 19; - const int capabilitiesLen = 12; - var descriptionLen = (from @join in joins select @join.Value into j select j.Metadata.Description.Length).Concat(new[] {11}).Max(); - - //build header - sb.Append(string.Format(stringFormatter, - string.Format("Join Number").PadRight(joinNumberLen, ' '), - string.Format("Join Span").PadRight(joinSpanLen, ' '), - string.Format("Description").PadRight(descriptionLen, ' '), - string.Format("Type").PadRight(typeLen, ' '), - string.Format("Capabilities").PadRight(capabilitiesLen, ' '))); - //build table seperator - sb.Append(string.Format(stringFormatter, - new string('-', joinNumberLen), - new string('-', joinSpanLen), - new string('-', descriptionLen), - new string('-', typeLen), - new string('-', capabilitiesLen))); - - foreach (var join in joins) - { - sb.Append(join.Value.GetMarkdownFormattedData(stringFormatter, descriptionLen)); - } - sb.Append("\r\n"); - return sb; - } - - /// - /// Attempts to find the matching key for the custom join and if found overwrites the default JoinData with the custom - /// - /// - /// - /// SetCustomJoinData method - /// - public void SetCustomJoinData(Dictionary joinData) - { - foreach (var customJoinData in joinData) - { - JoinDataComplete join; - - if (!Joins.TryGetValue(customJoinData.Key, out join)) - { - Debug.LogMessage(LogEventLevel.Verbose, "No matching key found in join map for: '{0}'", customJoinData.Key); - continue; - } - - if (join != null) - { - join.SetCustomJoinData(customJoinData.Value); - } - } - PrintJoinMapInfo(); } - - // /// - // /// Returns the join span for the join with the specified key - // /// - // /// - // /// - //public uint GetJoinSpanForKey(string key) - //{ - // return Joins.ContainsKey(key) ? Joins[key].JoinSpan : 0; - //} } /// - /// Read = Provides feedback to SIMPL - /// Write = Responds to sig values from SIMPL + /// Prints the join information to console /// - [Flags] - public enum eJoinCapabilities + public void PrintJoinMapInfo() { - /// - /// No capabilities - /// - None = 0, + var sb = JoinmapStringBuilder(); - /// - /// To SIMPL - /// - ToSIMPL = 1, - - /// - /// From SIMPL - /// - FromSIMPL = 2, - - /// - /// To and From SIMPL - /// - ToFromSIMPL = ToSIMPL | FromSIMPL, - - /// - /// To Fusion - /// - ToFusion = 4, - - /// - /// From Fusion - /// - FromFusion = 8, - - /// - /// To and From Fusion - /// - ToFromFusion = ToFusion | FromFusion, + CrestronConsole.ConsoleCommandResponse(sb.ToString()); } - /// - /// Enumeration of eJoinType values - /// - [Flags] - public enum eJoinType + private StringBuilder JoinmapStringBuilder() { - /// - /// No join type - /// - None = 0, + var sb = new StringBuilder(); - /// - /// Digital join - /// - Digital = 1, + var lineEnding = "\r\n"; - /// - /// Analog join - /// - Analog = 2, + var digitals = + Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Digital)) + .ToDictionary(j => j.Key, j => j.Value); - /// - /// Serial join - /// - Serial = 4, + var analogs = Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Analog)) + .ToDictionary(j => j.Key, j => j.Value); - /// - /// Digital and Analog join - /// - DigitalAnalog = Digital | Analog, + var serials = + Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Serial)) + .ToDictionary(j => j.Key, j => j.Value); - /// - /// Digital and Serial join - /// - DigitalSerial = Digital | Serial, + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Digital join count {digitalCount} Analog join count {analogCount} Serial join count {serialCount}", null, digitals.Count, analogs.Count, serials.Count); - /// - /// Analog and Serial join - /// - AnalogSerial = Analog | Serial, - - /// - /// Digital, Analog and Serial join - /// - DigitalAnalogSerial = Digital | Analog | Serial, - } - - /// - /// Represents a JoinMetadata - /// - public class JoinMetadata - { - private string _description; - /// - /// A description for the join to better describe its usage - /// - [JsonProperty("description")] - public string Description { get { return _description; } set { _description = value; } } - - /// - /// Gets or sets the JoinType - /// - [JsonProperty("joinType")] - public eJoinType JoinType { get; set; } + // Get the joins of each type and print them + sb.Append($"# {GetType().Name}\r\n"); + sb.Append(lineEnding); + sb.Append($"## Digitals{lineEnding}"); + sb.Append(lineEnding); + // Get the joins of each type and print them - /// - /// Gets or sets the JoinCapabilities - /// - [JsonProperty("joinCapabilities")] - public eJoinCapabilities JoinCapabilities { get; set; } + var digitalSb = AppendJoinList(GetSortedJoins(digitals)); + digitalSb.Append($"## Analogs{lineEnding}"); + digitalSb.Append(lineEnding); + + var analogSb = AppendJoinList(GetSortedJoins(analogs)); + analogSb.Append($"## Serials{lineEnding}"); + analogSb.Append(lineEnding); - /// - /// Gets or sets the ValidValues - /// - [JsonProperty("validValues")] - public string[] ValidValues { get; set; } + + var serialSb = AppendJoinList(GetSortedJoins(serials)); + sb.EnsureCapacity(sb.Length + digitalSb.Length + analogSb.Length + serialSb.Length); + sb.Append(digitalSb).Append(analogSb).Append(serialSb); + return sb; } /// - /// Data describing the join. Can be overridden from configuratino + /// Prints the join information to console /// - public class JoinData + public void MarkdownJoinMapInfo(string deviceKey, string bridgeKey) { - /// - /// Join number (based on join offset value) - /// - [JsonProperty("joinNumber")] - public uint JoinNumber { get; set; } + var pluginType = GetType().Name; - /// - /// Join range span. If join indicates the start of a range of joins, this indicated the maximum number of joins in the range - /// - [JsonProperty("joinSpan")] - public uint JoinSpan { get; set; } + CrestronConsole.ConsoleCommandResponse("{0}:\n", pluginType); + + + + WriteJoinmapMarkdown(JoinmapStringBuilder(), pluginType, bridgeKey, deviceKey); - /// - /// Gets or sets the AttributeName - /// - [JsonProperty("attributeName")] - public string AttributeName { get; set; } } - /// - /// A class to aggregate the JoinData and JoinMetadata for a join - /// - public class JoinDataComplete + private static void WriteJoinmapMarkdown(StringBuilder stringBuilder, string pluginType, string bridgeKey, string deviceKey) { - private uint _joinOffset; + var fileName = string.Format("{0}{1}{2}__{3}__{4}.md", Global.FilePathPrefix, "joinMaps/", pluginType, bridgeKey, deviceKey); - private JoinData _data; - - /// - /// Gets or sets the JoinMetadata - /// - public JoinMetadata Metadata { get; set; } - - /// - /// To store some future information as you please - /// - public object UserObject { get; private set; } - - /// - /// Constructor - /// - /// - /// - public JoinDataComplete(JoinData data, JoinMetadata metadata) + using (var sw = new StreamWriter(fileName)) { - _data = data; - Metadata = metadata; + sw.WriteLine(stringBuilder.ToString()); + CrestronConsole.ConsoleCommandResponse("Joinmap Readme generated and written to {0}", fileName); } - /// - /// GetMarkdownFormattedData method - /// - /// formatter to use - /// length of the description - /// - public string GetMarkdownFormattedData(string stringFormatter, int descriptionLen) + } + + /// + /// Returns a sorted list by JoinNumber + /// + /// + /// + static List> GetSortedJoins(Dictionary joins) + { + var sortedJoins = joins.ToList(); + + sortedJoins.Sort((pair1, pair2) => pair1.Value.JoinNumber.CompareTo(pair2.Value.JoinNumber)); + + return sortedJoins; + } + + + static StringBuilder AppendJoinList(List> joins) + { + var sb = new StringBuilder(); + const string stringFormatter = "| {0} | {1} | {2} | {3} | {4} |\r\n"; + const int joinNumberLen = 11; + const int joinSpanLen = 9; + const int typeLen = 19; + const int capabilitiesLen = 12; + var descriptionLen = (from @join in joins select @join.Value into j select j.Metadata.Description.Length).Concat(new[] {11}).Max(); + + //build header + sb.Append(string.Format(stringFormatter, + string.Format("Join Number").PadRight(joinNumberLen, ' '), + string.Format("Join Span").PadRight(joinSpanLen, ' '), + string.Format("Description").PadRight(descriptionLen, ' '), + string.Format("Type").PadRight(typeLen, ' '), + string.Format("Capabilities").PadRight(capabilitiesLen, ' '))); + //build table seperator + sb.Append(string.Format(stringFormatter, + new string('-', joinNumberLen), + new string('-', joinSpanLen), + new string('-', descriptionLen), + new string('-', typeLen), + new string('-', capabilitiesLen))); + + foreach (var join in joins) { + sb.Append(join.Value.GetMarkdownFormattedData(stringFormatter, descriptionLen)); + } + sb.Append("\r\n"); + return sb; + } - //Fixed Width Headers - var joinNumberLen = string.Format("Join Number").Length; - var joinSpanLen = string.Format("Join Span").Length; - var typeLen = string.Format("AnalogDigitalSerial").Length; - var capabilitiesLen = string.Format("ToFromFusion").Length; + /// + /// Attempts to find the matching key for the custom join and if found overwrites the default JoinData with the custom + /// + /// + public void SetCustomJoinData(Dictionary joinData) + { + foreach (var customJoinData in joinData) + { + JoinDataComplete join; - //Track which one failed, if it did - const string placeholder = "unknown"; - var dataArray = new Dictionary + if (!Joins.TryGetValue(customJoinData.Key, out join)) { - {"joinNumber", placeholder.PadRight(joinNumberLen, ' ')}, - {"joinSpan", placeholder.PadRight(joinSpanLen, ' ')}, - {"description", placeholder.PadRight(descriptionLen, ' ')}, - {"joinType", placeholder.PadRight(typeLen, ' ')}, - {"capabilities", placeholder.PadRight(capabilitiesLen, ' ')} - }; - - - try - { - dataArray["joinNumber"] = string.Format("{0}", JoinNumber.ToString(CultureInfo.InvariantCulture).ReplaceIfNullOrEmpty(placeholder)).PadRight(joinNumberLen, ' '); - dataArray["joinSpan"] = string.Format("{0}", JoinSpan.ToString(CultureInfo.InvariantCulture).ReplaceIfNullOrEmpty(placeholder)).PadRight(joinSpanLen, ' '); - dataArray["description"] = string.Format("{0}", Metadata.Description.ReplaceIfNullOrEmpty(placeholder)).PadRight(descriptionLen, ' '); - dataArray["joinType"] = string.Format("{0}", Metadata.JoinType.ToString().ReplaceIfNullOrEmpty(placeholder)).PadRight(typeLen, ' '); - dataArray["capabilities"] = string.Format("{0}", Metadata.JoinCapabilities.ToString().ReplaceIfNullOrEmpty(placeholder)).PadRight(capabilitiesLen, ' '); - - return string.Format(stringFormatter, - dataArray["joinNumber"], - dataArray["joinSpan"], - dataArray["description"], - dataArray["joinType"], - dataArray["capabilities"]); - + Debug.LogMessage(LogEventLevel.Verbose, "No matching key found in join map for: '{0}'", customJoinData.Key); + continue; } - catch (Exception e) + + if (join != null) { - //Don't Throw - we don't want to kill the system if this falls over - it's not mission critical. Print the error, use placeholder data - var errorKey = string.Empty; - foreach (var item in dataArray) - { - if (item.Value.TrimEnd() == placeholder) continue; - errorKey = item.Key; - break; - } - Debug.LogMessage(LogEventLevel.Information, "Unable to decode join metadata {1}- {0}", e.Message, !string.IsNullOrEmpty(errorKey) ? (' ' + errorKey) : string.Empty); - return string.Format(stringFormatter, - dataArray["joinNumber"], - dataArray["joinSpan"], - dataArray["description"], - dataArray["joinType"], - dataArray["capabilities"]); + join.SetCustomJoinData(customJoinData.Value); } } + PrintJoinMapInfo(); + } - /// - /// Sets the join offset value - /// - /// - public void SetJoinOffset(uint joinOffset) + ///// + ///// Returns the join number for the join with the specified key + ///// + ///// + ///// + //public uint GetJoinForKey(string key) + //{ + // return Joins.ContainsKey(key) ? Joins[key].JoinNumber : 0; + //} + + + ///// + ///// Returns the join span for the join with the specified key + ///// + ///// + ///// + //public uint GetJoinSpanForKey(string key) + //{ + // return Joins.ContainsKey(key) ? Joins[key].JoinSpan : 0; + //} +} + +/// +/// Read = Provides feedback to SIMPL +/// Write = Responds to sig values from SIMPL +/// +[Flags] +public enum eJoinCapabilities +{ + None = 0, + ToSIMPL = 1, + FromSIMPL = 2, + ToFromSIMPL = ToSIMPL | FromSIMPL, + ToFusion = 4, + FromFusion = 8, + ToFromFusion = ToFusion | FromFusion, +} + +[Flags] +public enum eJoinType +{ + None = 0, + Digital = 1, + Analog = 2, + Serial = 4, + DigitalAnalog = Digital | Analog, + DigitalSerial = Digital | Serial, + AnalogSerial = Analog | Serial, + DigitalAnalogSerial = Digital | Analog | Serial, +} + +/// +/// Metadata describing the join +/// +public class JoinMetadata +{ + private string _description; + /// + /// A description for the join to better describe its usage + /// + [JsonProperty("description")] + public string Description { get { return _description; } set { _description = value; } } + /// + /// Signal type(s) + /// + [JsonProperty("joinType")] + public eJoinType JoinType { get; set; } + /// + /// Indicates whether the join is read and/or write + /// + [JsonProperty("joinCapabilities")] + public eJoinCapabilities JoinCapabilities { get; set; } + /// + /// Indicates a set of valid values (particularly if this translates to an enum + /// + [JsonProperty("validValues")] + public string[] ValidValues { get; set; } + +} + +/// +/// Data describing the join. Can be overridden from configuratino +/// +public class JoinData +{ + /// + /// Join number (based on join offset value) + /// + [JsonProperty("joinNumber")] + public uint JoinNumber { get; set; } + /// + /// Join range span. If join indicates the start of a range of joins, this indicated the maximum number of joins in the range + /// + [JsonProperty("joinSpan")] + public uint JoinSpan { get; set; } + /// + /// Fusion Attribute Name (optional) + /// + [JsonProperty("attributeName")] + public string AttributeName { get; set; } +} + +/// +/// A class to aggregate the JoinData and JoinMetadata for a join +/// +public class JoinDataComplete +{ + private uint _joinOffset; + + private JoinData _data; + public JoinMetadata Metadata { get; set; } + /// + /// To store some future information as you please + /// + public object UserObject { get; private set; } + + public JoinDataComplete(JoinData data, JoinMetadata metadata) + { + _data = data; + Metadata = metadata; + } + + public string GetMarkdownFormattedData(string stringFormatter, int descriptionLen) + { + + //Fixed Width Headers + var joinNumberLen = string.Format("Join Number").Length; + var joinSpanLen = string.Format("Join Span").Length; + var typeLen = string.Format("AnalogDigitalSerial").Length; + var capabilitiesLen = string.Format("ToFromFusion").Length; + + //Track which one failed, if it did + const string placeholder = "unknown"; + var dataArray = new Dictionary { - _joinOffset = joinOffset; + {"joinNumber", placeholder.PadRight(joinNumberLen, ' ')}, + {"joinSpan", placeholder.PadRight(joinSpanLen, ' ')}, + {"description", placeholder.PadRight(descriptionLen, ' ')}, + {"joinType", placeholder.PadRight(typeLen, ' ')}, + {"capabilities", placeholder.PadRight(capabilitiesLen, ' ')} + }; + + + try + { + dataArray["joinNumber"] = string.Format("{0}", JoinNumber.ToString(CultureInfo.InvariantCulture).ReplaceIfNullOrEmpty(placeholder)).PadRight(joinNumberLen, ' '); + dataArray["joinSpan"] = string.Format("{0}", JoinSpan.ToString(CultureInfo.InvariantCulture).ReplaceIfNullOrEmpty(placeholder)).PadRight(joinSpanLen, ' '); + dataArray["description"] = string.Format("{0}", Metadata.Description.ReplaceIfNullOrEmpty(placeholder)).PadRight(descriptionLen, ' '); + dataArray["joinType"] = string.Format("{0}", Metadata.JoinType.ToString().ReplaceIfNullOrEmpty(placeholder)).PadRight(typeLen, ' '); + dataArray["capabilities"] = string.Format("{0}", Metadata.JoinCapabilities.ToString().ReplaceIfNullOrEmpty(placeholder)).PadRight(capabilitiesLen, ' '); + + return string.Format(stringFormatter, + dataArray["joinNumber"], + dataArray["joinSpan"], + dataArray["description"], + dataArray["joinType"], + dataArray["capabilities"]); + } - - /// - /// The join number (including the offset) - /// - public uint JoinNumber + catch (Exception e) { - get { return _data.JoinNumber+ _joinOffset; } - set { _data.JoinNumber = value; } - } - - /// - /// The join span - /// - public uint JoinSpan - { - get { return _data.JoinSpan; } - } - - /// - /// The attribute name - /// - public string AttributeName - { - get { return _data.AttributeName; } - } - - /// - /// SetCustomJoinData method - /// - public void SetCustomJoinData(JoinData customJoinData) - { - _data = customJoinData; - } - - /// - /// GetNameAttribute method - /// - public string GetNameAttribute(MemberInfo memberInfo) - { - var name = string.Empty; - var attribute = (JoinNameAttribute)Attribute.GetCustomAttribute(memberInfo, typeof(JoinNameAttribute)); - - if (attribute == null) return name; - - name = attribute.Name; - Debug.LogMessage(LogEventLevel.Verbose, "JoinName Attribute value: {0}", name); - return name; + //Don't Throw - we don't want to kill the system if this falls over - it's not mission critical. Print the error, use placeholder data + var errorKey = string.Empty; + foreach (var item in dataArray) + { + if (item.Value.TrimEnd() == placeholder) continue; + errorKey = item.Key; + break; + } + Debug.LogMessage(LogEventLevel.Information, "Unable to decode join metadata {1}- {0}", e.Message, !string.IsNullOrEmpty(errorKey) ? (' ' + errorKey) : string.Empty); + return string.Format(stringFormatter, + dataArray["joinNumber"], + dataArray["joinSpan"], + dataArray["description"], + dataArray["joinType"], + dataArray["capabilities"]); } } - /// - /// Represents a JoinNameAttribute + /// Sets the join offset value /// - [AttributeUsage(AttributeTargets.All)] - public class JoinNameAttribute : Attribute + /// + public void SetJoinOffset(uint joinOffset) { - private string _Name; + _joinOffset = joinOffset; + } - /// - /// Constructor - /// - /// name of the attribute - public JoinNameAttribute(string name) - { - Debug.LogMessage(LogEventLevel.Verbose, "Setting Attribute Name: {0}",null, name); - _Name = name; - } + /// + /// The join number (including the offset) + /// + public uint JoinNumber + { + get { return _data.JoinNumber+ _joinOffset; } + set { _data.JoinNumber = value; } + } - /// - /// Gets the Name - /// - public string Name - { - get { return _Name; } - } + public uint JoinSpan + { + get { return _data.JoinSpan; } + } + + public string AttributeName + { + get { return _data.AttributeName; } + } + + public void SetCustomJoinData(JoinData customJoinData) + { + _data = customJoinData; + } + + public string GetNameAttribute(MemberInfo memberInfo) + { + var name = string.Empty; + var attribute = (JoinNameAttribute)Attribute.GetCustomAttribute(memberInfo, typeof(JoinNameAttribute)); + + if (attribute == null) return name; + + name = attribute.Name; + Debug.LogMessage(LogEventLevel.Verbose, "JoinName Attribute value: {0}", name); + return name; + } +} + + + +[AttributeUsage(AttributeTargets.All)] +public class JoinNameAttribute : Attribute +{ + private string _Name; + + public JoinNameAttribute(string name) + { + Debug.LogMessage(LogEventLevel.Verbose, "Setting Attribute Name: {0}",null, name); + _Name = name; + } + + public string Name + { + get { return _Name; } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/License/EssentialsLicenseManager.cs b/src/PepperDash.Essentials.Core/License/EssentialsLicenseManager.cs index ded7f699..9a6d2434 100644 --- a/src/PepperDash.Essentials.Core/License/EssentialsLicenseManager.cs +++ b/src/PepperDash.Essentials.Core/License/EssentialsLicenseManager.cs @@ -11,104 +11,103 @@ using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.License +namespace PepperDash.Essentials.License; + +/// +/// Abstract base class for License Managers +/// +public abstract class LicenseManager { /// - /// Abstract base class for License Managers + /// Gets or sets the LicenseIsValid /// - public abstract class LicenseManager + public BoolFeedback LicenseIsValid { get; protected set; } + + /// + /// Gets or sets the LicenseMessage + /// + public StringFeedback LicenseMessage { get; protected set; } + + /// + /// Gets or sets the LicenseLog + /// + public StringFeedback LicenseLog { get; protected set; } + + /// + /// Constructor + /// + protected LicenseManager() { - /// - /// Gets or sets the LicenseIsValid - /// - public BoolFeedback LicenseIsValid { get; protected set; } - - /// - /// Gets or sets the LicenseMessage - /// - public StringFeedback LicenseMessage { get; protected set; } - - /// - /// Gets or sets the LicenseLog - /// - public StringFeedback LicenseLog { get; protected set; } - - /// - /// Constructor - /// - protected LicenseManager() - { - CrestronConsole.AddNewConsoleCommand( - s => CrestronConsole.ConsoleCommandResponse(GetStatusString()), - "licensestatus", "shows license and related data", - ConsoleAccessLevelEnum.AccessOperator); - } - - /// - /// Gets the status string for console command - /// - protected abstract string GetStatusString(); + CrestronConsole.AddNewConsoleCommand( + s => CrestronConsole.ConsoleCommandResponse(GetStatusString()), + "licensestatus", "shows license and related data", + ConsoleAccessLevelEnum.AccessOperator); } /// - /// Represents a MockEssentialsLicenseManager + /// Gets the status string for console command /// - public class MockEssentialsLicenseManager : LicenseManager + protected abstract string GetStatusString(); +} + +/// +/// Represents a MockEssentialsLicenseManager +/// +public class MockEssentialsLicenseManager : LicenseManager +{ + /// + /// Returns the singleton mock license manager for this app + /// + public static MockEssentialsLicenseManager Manager { - /// - /// Returns the singleton mock license manager for this app - /// - public static MockEssentialsLicenseManager Manager + get { - get - { - if (_Manager == null) - _Manager = new MockEssentialsLicenseManager(); - return _Manager; - } - } - static MockEssentialsLicenseManager _Manager; - - bool IsValid; - - MockEssentialsLicenseManager() : base() - { - LicenseIsValid = new BoolFeedback("LicenseIsValid", - () => { return IsValid; }); - CrestronConsole.AddNewConsoleCommand( - s => SetFromConsole(s.Equals("true", StringComparison.OrdinalIgnoreCase)), - "mocklicense", "true or false for testing", ConsoleAccessLevelEnum.AccessOperator); - - bool valid; - var err = CrestronDataStoreStatic.GetGlobalBoolValue("MockLicense", out valid); - if (err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - SetIsValid(valid); - else if (err == CrestronDataStore.CDS_ERROR.CDS_RECORD_NOT_FOUND) - CrestronDataStoreStatic.SetGlobalBoolValue("MockLicense", false); - else - CrestronConsole.PrintLine("Error restoring Mock License setting: {0}", err); - } - - void SetIsValid(bool isValid) - { - IsValid = isValid; - CrestronDataStoreStatic.SetGlobalBoolValue("MockLicense", isValid); - Debug.LogMessage(LogEventLevel.Information, "Mock License is{0} valid", IsValid ? "" : " not"); - LicenseIsValid.FireUpdate(); - } - - void SetFromConsole(bool isValid) - { - SetIsValid(isValid); - } - - /// - /// Gets the status string for console command - /// - /// license status valid or invalid - protected override string GetStatusString() - { - return string.Format("License Status: {0}", IsValid ? "Valid" : "Not Valid"); + if (_Manager == null) + _Manager = new MockEssentialsLicenseManager(); + return _Manager; } } + static MockEssentialsLicenseManager _Manager; + + bool IsValid; + + MockEssentialsLicenseManager() : base() + { + LicenseIsValid = new BoolFeedback("LicenseIsValid", + () => { return IsValid; }); + CrestronConsole.AddNewConsoleCommand( + s => SetFromConsole(s.Equals("true", StringComparison.OrdinalIgnoreCase)), + "mocklicense", "true or false for testing", ConsoleAccessLevelEnum.AccessOperator); + + bool valid; + var err = CrestronDataStoreStatic.GetGlobalBoolValue("MockLicense", out valid); + if (err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + SetIsValid(valid); + else if (err == CrestronDataStore.CDS_ERROR.CDS_RECORD_NOT_FOUND) + CrestronDataStoreStatic.SetGlobalBoolValue("MockLicense", false); + else + CrestronConsole.PrintLine("Error restoring Mock License setting: {0}", err); + } + + void SetIsValid(bool isValid) + { + IsValid = isValid; + CrestronDataStoreStatic.SetGlobalBoolValue("MockLicense", isValid); + Debug.LogMessage(LogEventLevel.Information, "Mock License is{0} valid", IsValid ? "" : " not"); + LicenseIsValid.FireUpdate(); + } + + void SetFromConsole(bool isValid) + { + SetIsValid(isValid); + } + + /// + /// Gets the status string for console command + /// + /// license status valid or invalid + protected override string GetStatusString() + { + return string.Format("License Status: {0}", IsValid ? "Valid" : "Not Valid"); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Lighting/Lighting Interfaces.cs b/src/PepperDash.Essentials.Core/Lighting/Lighting Interfaces.cs index 642ca6f8..dfc6cd28 100644 --- a/src/PepperDash.Essentials.Core/Lighting/Lighting Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Lighting/Lighting Interfaces.cs @@ -1,118 +1,118 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core.Lighting +namespace PepperDash.Essentials.Core.Lighting; + + +/// +/// Defines the contract for ILightingScenes +/// +public interface ILightingScenes { /// - /// Defines the contract for ILightingScenes + /// Fires when the lighting scene changes /// - public interface ILightingScenes - { - /// - /// Fires when the lighting scene changes - /// - event EventHandler LightingSceneChange; - - /// - /// Gets the list of LightingScenes - /// - List LightingScenes { get; } - - /// - /// Selects the given LightingScene - /// - /// scene to select - void SelectScene(LightingScene scene); - - /// - /// Gets the currently selected LightingScene - /// - LightingScene CurrentLightingScene { get; } - - } + event EventHandler LightingSceneChange; /// - /// Defines the contract for ILightingScenesDynamic + /// Gets the list of LightingScenes /// - public interface ILightingScenesDynamic : ILightingScenes - { - /// - /// Fires when the lighting scenes are updated - /// - event EventHandler LightingScenesUpdated; - } + List LightingScenes { get; } /// - /// Defines the contract for ILightingMasterRaiseLower + /// Selects the given LightingScene /// - public interface ILightingMasterRaiseLower - { - /// - /// Raises the master level - /// - void MasterRaise(); - - /// - /// Lowers the master level - /// - void MasterLower(); - - /// - /// Stops raising or lowering the master level - /// - void MasterRaiseLowerStop(); - } + /// scene to select + void SelectScene(LightingScene scene); /// - /// Defines the contract for ILightingLoad + /// Gets the currently selected LightingScene /// - public interface ILightingLoad - { - /// - /// Sets the load level - /// - /// level to set - void SetLoadLevel(int level); + LightingScene CurrentLightingScene { get; } - /// - /// Raises the load level - /// - void Raise(); +} - /// - /// Lowers the load level - /// - void Lower(); +/// +/// Defines the contract for ILightingScenesDynamic +/// +public interface ILightingScenesDynamic : ILightingScenes +{ + /// + /// Fires when the lighting scenes are updated + /// + event EventHandler LightingScenesUpdated; +} - /// - /// feedback of the current load level - /// - IntFeedback LoadLevelFeedback { get; } - - /// - /// feedback of whether the load is on - /// - BoolFeedback LoadIsOnFeedback { get; } - } +/// +/// Defines the contract for ILightingMasterRaiseLower +/// +public interface ILightingMasterRaiseLower +{ + /// + /// Raises the master level + /// + void MasterRaise(); /// - /// Represents a LightingSceneChangeEventArgs + /// Lowers the master level /// - public class LightingSceneChangeEventArgs : EventArgs + void MasterLower(); + + /// + /// Stops raising or lowering the master level + /// + void MasterRaiseLowerStop(); +} + +/// +/// Defines the contract for ILightingLoad +/// +public interface ILightingLoad +{ + /// + /// Sets the load level + /// + /// level to set + void SetLoadLevel(int level); + + /// + /// Raises the load level + /// + void Raise(); + + /// + /// Lowers the load level + /// + void Lower(); + + /// + /// feedback of the current load level + /// + IntFeedback LoadLevelFeedback { get; } + + /// + /// feedback of whether the load is on + /// + BoolFeedback LoadIsOnFeedback { get; } +} + +/// +/// Represents a LightingSceneChangeEventArgs +/// +public class LightingSceneChangeEventArgs : EventArgs +{ + /// + /// Gets or sets the CurrentLightingScene + /// + public LightingScene CurrentLightingScene { get; private set; } + + /// + /// Constructor for LightingSceneChangeEventArgs + /// + /// The lighting scene that changed + public LightingSceneChangeEventArgs(LightingScene scene) { - /// - /// Gets or sets the CurrentLightingScene - /// - public LightingScene CurrentLightingScene { get; private set; } - - /// - /// Constructor for LightingSceneChangeEventArgs - /// - /// The lighting scene that changed - public LightingSceneChangeEventArgs(LightingScene scene) - { - CurrentLightingScene = scene; - } + CurrentLightingScene = scene; } +} -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs b/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs index a15e0dc4..29cd4e3c 100644 --- a/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs +++ b/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs @@ -11,286 +11,247 @@ using PepperDash.Essentials.Core.CrestronIO; using Serilog.Events; -namespace PepperDash.Essentials.Core.Privacy -{ - /// - /// Used for applications where one or more microphones with momentary contact closure outputs are used to - /// toggle the privacy state of the room. Privacy state feedback is represented - /// - public class MicrophonePrivacyController : EssentialsDevice - { - MicrophonePrivacyControllerConfig Config; +namespace PepperDash.Essentials.Core.Privacy; - bool initialized; +/// +/// Used for applications where one or more microphones with momentary contact closure outputs are used to +/// toggle the privacy state of the room. Privacy state feedback is represented +/// +public class MicrophonePrivacyController : EssentialsDevice +{ + MicrophonePrivacyControllerConfig Config; + + bool initialized; /// /// Gets or sets whether LED control is enabled /// - public bool EnableLeds + public bool EnableLeds + { + get { - get - { - return _enableLeds; - } - set - { - _enableLeds = value; + return _enableLeds; + } + set + { + _enableLeds = value; - if (initialized) + if (initialized) + { + if (value) { - if (value) - { - CheckPrivacyMode(); - SetLedStates(); - } - else - TurnOffAllLeds(); + CheckPrivacyMode(); + SetLedStates(); } + else + TurnOffAllLeds(); } } - bool _enableLeds; + } + + bool _enableLeds; - /// - /// Gets or sets the Inputs - /// - public List Inputs { get; private set; } + public List Inputs { get; private set; } - /// - /// Gets or sets the RedLedRelay - /// - public GenericRelayDevice RedLedRelay { get; private set; } - bool _redLedRelayState; + public GenericRelayDevice RedLedRelay { get; private set; } + bool _redLedRelayState; - /// - /// Gets or sets the GreenLedRelay - /// - public GenericRelayDevice GreenLedRelay { get; private set; } - bool _greenLedRelayState; + public GenericRelayDevice GreenLedRelay { get; private set; } + bool _greenLedRelayState; - /// - /// Gets or sets the PrivacyDevice - /// - public IPrivacy PrivacyDevice { get; private set; } + public IPrivacy PrivacyDevice { get; private set; } - /// - /// Constructor for MicrophonePrivacyController - /// - /// key of the controller device - /// configuration for the controller device - public MicrophonePrivacyController(string key, MicrophonePrivacyControllerConfig config) : - base(key) + public MicrophonePrivacyController(string key, MicrophonePrivacyControllerConfig config) : + base(key) + { + Config = config; + + Inputs = new List(); + } + + public override bool CustomActivate() + { + foreach (var i in Config.Inputs) { - Config = config; + var input = DeviceManager.GetDeviceForKey(i.DeviceKey) as IDigitalInput; - Inputs = new List(); + if(input != null) + AddInput(input); } - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() - { - foreach (var i in Config.Inputs) - { - var input = DeviceManager.GetDeviceForKey(i.DeviceKey) as IDigitalInput; + var greenLed = DeviceManager.GetDeviceForKey(Config.GreenLedRelay.DeviceKey) as GenericRelayDevice; - if(input != null) - AddInput(input); - } + if (greenLed != null) + GreenLedRelay = greenLed; + else + Debug.LogMessage(LogEventLevel.Information, this, "Unable to add Green LED device"); - var greenLed = DeviceManager.GetDeviceForKey(Config.GreenLedRelay.DeviceKey) as GenericRelayDevice; + var redLed = DeviceManager.GetDeviceForKey(Config.RedLedRelay.DeviceKey) as GenericRelayDevice; - if (greenLed != null) - GreenLedRelay = greenLed; - else - Debug.LogMessage(LogEventLevel.Information, this, "Unable to add Green LED device"); + if (redLed != null) + RedLedRelay = redLed; + else + Debug.LogMessage(LogEventLevel.Information, this, "Unable to add Red LED device"); - var redLed = DeviceManager.GetDeviceForKey(Config.RedLedRelay.DeviceKey) as GenericRelayDevice; + AddPostActivationAction(() => { + PrivacyDevice.PrivacyModeIsOnFeedback.OutputChange -= PrivacyModeIsOnFeedback_OutputChange; + PrivacyDevice.PrivacyModeIsOnFeedback.OutputChange += PrivacyModeIsOnFeedback_OutputChange; + }); - if (redLed != null) - RedLedRelay = redLed; - else - Debug.LogMessage(LogEventLevel.Information, this, "Unable to add Red LED device"); + initialized = true; - AddPostActivationAction(() => { - PrivacyDevice.PrivacyModeIsOnFeedback.OutputChange -= PrivacyModeIsOnFeedback_OutputChange; - PrivacyDevice.PrivacyModeIsOnFeedback.OutputChange += PrivacyModeIsOnFeedback_OutputChange; - }); + return base.CustomActivate(); + } - initialized = true; + #region Overrides of Device - return base.CustomActivate(); - } + public override void Initialize() + { + CheckPrivacyMode(); + } - #region Overrides of Device + #endregion - /// - /// Initialize method - /// - /// - public override void Initialize() - { - CheckPrivacyMode(); - } + public void SetPrivacyDevice(IPrivacy privacyDevice) + { + PrivacyDevice = privacyDevice; + } - #endregion - - /// - /// SetPrivacyDevice method - /// - public void SetPrivacyDevice(IPrivacy privacyDevice) - { - PrivacyDevice = privacyDevice; - } - - void PrivacyModeIsOnFeedback_OutputChange(object sender, EventArgs e) - { + void PrivacyModeIsOnFeedback_OutputChange(object sender, EventArgs e) + { Debug.LogMessage(LogEventLevel.Debug, this, "Privacy mode change: {0}", sender as BoolFeedback); - CheckPrivacyMode(); - } + CheckPrivacyMode(); + } - void CheckPrivacyMode() + void CheckPrivacyMode() + { + if (PrivacyDevice != null) { - if (PrivacyDevice != null) - { - var privacyState = PrivacyDevice.PrivacyModeIsOnFeedback.BoolValue; + var privacyState = PrivacyDevice.PrivacyModeIsOnFeedback.BoolValue; - if (privacyState) - TurnOnRedLeds(); - else - TurnOnGreenLeds(); - } - } - - void AddInput(IDigitalInput input) - { - Inputs.Add(input); - - input.InputStateFeedback.OutputChange += InputStateFeedback_OutputChange; - } - - void RemoveInput(IDigitalInput input) - { - var tempInput = Inputs.FirstOrDefault(i => i.Equals(input)); - - if (tempInput != null) - tempInput.InputStateFeedback.OutputChange -= InputStateFeedback_OutputChange; - - Inputs.Remove(input); - } - - void SetRedLedRelay(GenericRelayDevice relay) - { - RedLedRelay = relay; - } - - void SetGreenLedRelay(GenericRelayDevice relay) - { - GreenLedRelay = relay; - } - - /// - /// Check the state of the input change and handle accordingly - /// - /// - /// - void InputStateFeedback_OutputChange(object sender, EventArgs e) - { - if ((sender as BoolFeedback).BoolValue == true) - TogglePrivacyMute(); - } - - /// - /// Toggles the state of the privacy mute - /// - public void TogglePrivacyMute() - { - PrivacyDevice.PrivacyModeToggle(); - } - - void TurnOnRedLeds() - { - _greenLedRelayState = false; - _redLedRelayState = true; - SetLedStates(); - } - - void TurnOnGreenLeds() - { - _redLedRelayState = false; - _greenLedRelayState = true; - SetLedStates(); - } - - /// - /// If enabled, sets the actual state of the relays - /// - void SetLedStates() - { - if (_enableLeds) - { - SetRelayStates(); - } + if (privacyState) + TurnOnRedLeds(); else - TurnOffAllLeds(); + TurnOnGreenLeds(); } + } - /// - /// Turns off all LEDs - /// - void TurnOffAllLeds() - { - _redLedRelayState = false; - _greenLedRelayState = false; + void AddInput(IDigitalInput input) + { + Inputs.Add(input); - SetRelayStates(); - } + input.InputStateFeedback.OutputChange += InputStateFeedback_OutputChange; + } - void SetRelayStates() - { - if (RedLedRelay != null) - { - if (_redLedRelayState) - RedLedRelay.CloseRelay(); - else - RedLedRelay.OpenRelay(); - } + void RemoveInput(IDigitalInput input) + { + var tempInput = Inputs.FirstOrDefault(i => i.Equals(input)); - if(GreenLedRelay != null) - { - if (_greenLedRelayState) - GreenLedRelay.CloseRelay(); - else - GreenLedRelay.OpenRelay(); - } - } + if (tempInput != null) + tempInput.InputStateFeedback.OutputChange -= InputStateFeedback_OutputChange; + + Inputs.Remove(input); + } + + void SetRedLedRelay(GenericRelayDevice relay) + { + RedLedRelay = relay; + } + + void SetGreenLedRelay(GenericRelayDevice relay) + { + GreenLedRelay = relay; } /// - /// Represents a MicrophonePrivacyControllerFactory + /// Check the state of the input change and handle accordingly /// - public class MicrophonePrivacyControllerFactory : EssentialsDeviceFactory + /// + /// + void InputStateFeedback_OutputChange(object sender, EventArgs e) { - /// - /// Constructor for MicrophonePrivacyControllerFactory - /// - public MicrophonePrivacyControllerFactory() - { - TypeNames = new List() { "microphoneprivacycontroller" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MIcrophonePrivacyController Device"); - var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); - - return new Core.Privacy.MicrophonePrivacyController(dc.Key, props); - } + if ((sender as BoolFeedback).BoolValue == true) + TogglePrivacyMute(); } + /// + /// Toggles the state of the privacy mute + /// + public void TogglePrivacyMute() + { + PrivacyDevice.PrivacyModeToggle(); + } + + void TurnOnRedLeds() + { + _greenLedRelayState = false; + _redLedRelayState = true; + SetLedStates(); + } + + void TurnOnGreenLeds() + { + _redLedRelayState = false; + _greenLedRelayState = true; + SetLedStates(); + } + + /// + /// If enabled, sets the actual state of the relays + /// + void SetLedStates() + { + if (_enableLeds) + { + SetRelayStates(); + } + else + TurnOffAllLeds(); + } + + /// + /// Turns off all LEDs + /// + void TurnOffAllLeds() + { + _redLedRelayState = false; + _greenLedRelayState = false; + + SetRelayStates(); + } + + void SetRelayStates() + { + if (RedLedRelay != null) + { + if (_redLedRelayState) + RedLedRelay.CloseRelay(); + else + RedLedRelay.OpenRelay(); + } + + if(GreenLedRelay != null) + { + if (_greenLedRelayState) + GreenLedRelay.CloseRelay(); + else + GreenLedRelay.OpenRelay(); + } + } +} + +public class MicrophonePrivacyControllerFactory : EssentialsDeviceFactory +{ + public MicrophonePrivacyControllerFactory() + { + TypeNames = new List() { "microphoneprivacycontroller" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MIcrophonePrivacyController Device"); + var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); + + return new Core.Privacy.MicrophonePrivacyController(dc.Key, props); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyControllerConfig.cs b/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyControllerConfig.cs index 902b51ed..d5df4531 100644 --- a/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyControllerConfig.cs +++ b/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyControllerConfig.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; - -using PepperDash.Essentials.Core.CrestronIO; +using System.Collections.Generic; namespace PepperDash.Essentials.Core.Privacy { diff --git a/src/PepperDash.Essentials.Core/Monitoring/CrestronGenericBaseCommunicationMonitor.cs b/src/PepperDash.Essentials.Core/Monitoring/CrestronGenericBaseCommunicationMonitor.cs index dc9406a3..2fce0783 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/CrestronGenericBaseCommunicationMonitor.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/CrestronGenericBaseCommunicationMonitor.cs @@ -11,8 +11,8 @@ using System.ComponentModel; using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// /// @@ -68,5 +68,4 @@ namespace PepperDash.Essentials.Core else StartErrorTimers(); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Monitoring/GenericCommunicationMonitor.cs b/src/PepperDash.Essentials.Core/Monitoring/GenericCommunicationMonitor.cs index 34f074d9..350ad893 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/GenericCommunicationMonitor.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/GenericCommunicationMonitor.cs @@ -3,321 +3,319 @@ using PepperDash.Core; using System.Threading; using PepperDash.Core.Logging; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Used for monitoring comms that are IBasicCommunication. Will send a poll string and provide an event when +/// statuses change. +/// Default monitoring uses TextReceived event on Client. +/// +public class GenericCommunicationMonitor : StatusMonitorBase { - /// - /// Used for monitoring comms that are IBasicCommunication. Will send a poll string and provide an event when - /// statuses change. - /// Default monitoring uses TextReceived event on Client. - /// - public class GenericCommunicationMonitor : StatusMonitorBase - { - /// - /// Gets the Client being monitored - /// - public IBasicCommunication Client { get; private set; } - - /// - /// Will monitor Client.BytesReceived if set to true. Otherwise the default is to monitor Client.TextReceived - /// - public bool MonitorBytesReceived { get; private set; } - - /// - /// Return true if the Client is ISocketStatus - /// - public bool IsSocket => Client is ISocketStatus; - - private readonly string PollString; - private readonly Action PollAction; - private readonly long PollTime; - - private Timer PollTimer; - - private SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); - - /// - /// GenericCommunicationMonitor constructor - /// - /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected - /// - /// Parent device - /// Communications Client - /// Time in MS for polling - /// Warning time in MS. If a message is not received before this elapsed time the status will be Warning - /// Error time in MS. If a message is not received before this elapsed time the status will be Error - /// string to send for polling - /// Poll time must be less than warning and error time - public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, - long warningTime, long errorTime, string pollString) : - base(parent, warningTime, errorTime) - { - if (pollTime > warningTime || pollTime > errorTime) - throw new ArgumentException("pollTime must be less than warning or errorTime"); - - Client = client; - PollTime = pollTime; - PollString = pollString; - - if (IsSocket) - { - (Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange; - } - } - - /// - /// GenericCommunicationMonitor constructor with a bool to specify whether to monitor BytesReceived - /// - /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected - /// - /// Parent device - /// Communications Client - /// Time in MS for polling - /// Warning time in MS. If a message is not received before this elapsed time the status will be Warning - /// Error time in MS. If a message is not received before this elapsed time the status will be Error - /// string to send for polling - /// Use bytesReceived event instead of textReceived when true - public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, - long warningTime, long errorTime, string pollString, bool monitorBytesReceived) : - this(parent, client, pollTime, warningTime, errorTime, pollString) - { - MonitorBytesReceived = monitorBytesReceived; - } - - /// - /// GenericCommunicationMonitor constructor with a poll action instead of a poll string - /// - /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected - /// - /// Parent device - /// Communications Client - /// Time in MS for polling - /// Warning time in MS. If a message is not received before this elapsed time the status will be Warning - /// Error time in MS. If a message is not received before this elapsed time the status will be Error - /// Action to execute for polling - /// Poll time must be less than warning and error time - public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, - long warningTime, long errorTime, Action pollAction) : - base(parent, warningTime, errorTime) - { - if (pollTime > warningTime || pollTime > errorTime) - throw new ArgumentException("pollTime must be less than warning or errorTime"); - //if (pollTime < 5000) - // throw new ArgumentException("pollTime cannot be less than 5000 ms"); - - Client = client; - PollTime = pollTime; - PollAction = pollAction; - - if (IsSocket) - { - (Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange; - } - - } - - /// - /// GenericCommunicationMonitor constructor with a poll action instead of a poll string and a bool to specify whether to monitor BytesReceived - /// - /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected - /// - /// Parent device - /// Communications Client - /// Time in MS for polling - /// Warning time in MS. If a message is not received before this elapsed time the status will be Warning - /// Error time in MS. If a message is not received before this elapsed time the status will be Error - /// Action to execute for polling - /// Use bytesReceived event instead of textReceived when true - public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, - long warningTime, long errorTime, Action pollAction, bool monitorBytesReceived) : - this(parent, client, pollTime, warningTime, errorTime, pollAction) - { - MonitorBytesReceived = monitorBytesReceived; - } - - - /// - /// GenericCommunicationMonitor constructor with a config object - /// - /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected - /// - /// Parent Device - /// Communications Client - /// Communication Monitor Config object - public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, - CommunicationMonitorConfig props) : - this(parent, client, props.PollInterval, props.TimeToWarning, props.TimeToError, props.PollString) - { - if (IsSocket) - { - (Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange; - } - } - - /// - /// GenericCommunicationMonitor constructor with a config object and a bool to specify whether to monitor BytesReceived - /// - /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected - /// - /// Parent Device - /// Communications Client - /// Communication Monitor Config object - /// Use bytesReceived event instead of textReceived when true - public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, CommunicationMonitorConfig props, bool monitorBytesReceived) : - this(parent, client, props.PollInterval, props.TimeToWarning, props.TimeToError, props.PollString) - { - MonitorBytesReceived = monitorBytesReceived; - } - - /// - /// Start the poll cycle - /// - public override void Start() - { - if (MonitorBytesReceived) - { - Client.BytesReceived -= Client_BytesReceived; - Client.BytesReceived += Client_BytesReceived; - } - else - { - Client.TextReceived -= Client_TextReceived; - Client.TextReceived += Client_TextReceived; - } - - BeginPolling(); - } - - private void Socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) - { - if (!e.Client.IsConnected) - { - // Immediately stop polling and notify that device is offline - Stop(); - Status = MonitorStatus.InError; - ResetErrorTimers(); - } - else - { - // Start polling and set status to unknow and let poll result update the status to IsOk when a response is received - Status = MonitorStatus.StatusUnknown; - Start(); - } - } - - private void BeginPolling() - { - try - { - semaphore.Wait(); - { - if (PollTimer != null) - { - return; - } - - PollTimer = new Timer(o => Poll(), null, 0, PollTime); - } - } - finally - { - semaphore.Release(); - } - } - - /// - /// Stop method - /// - /// - public override void Stop() - { - if(MonitorBytesReceived) - { - Client.BytesReceived -= Client_BytesReceived; - } - else - { - Client.TextReceived -= Client_TextReceived; - } - - StopErrorTimers(); - - if (PollTimer == null) - { - return; - } - - PollTimer.Dispose(); - PollTimer = null; - } - - private void Client_TextReceived(object sender, GenericCommMethodReceiveTextArgs e) - { - DataReceived(); - } - - private void Client_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) - { - DataReceived(); - } - - private void DataReceived() - { - Status = MonitorStatus.IsOk; - ResetErrorTimers(); - } - - private void Poll() - { - StartErrorTimers(); - if (Client.IsConnected) - { - //Debug.LogMessage(LogEventLevel.Verbose, this, "Polling"); - if(PollAction != null) - PollAction.Invoke(); - else - Client.SendText(PollString); - } - else - { - this.LogVerbose("Comm not connected"); - } - } - } + /// + /// Gets the Client being monitored + /// + public IBasicCommunication Client { get; private set; } /// - /// Represents a CommunicationMonitorConfig + /// Will monitor Client.BytesReceived if set to true. Otherwise the default is to monitor Client.TextReceived /// - public class CommunicationMonitorConfig - { - /// - /// Gets or sets the PollInterval - /// - public int PollInterval { get; set; } + public bool MonitorBytesReceived { get; private set; } - /// - /// Gets or sets the TimeToWarning - /// - public int TimeToWarning { get; set; } + /// + /// Return true if the Client is ISocketStatus + /// + public bool IsSocket => Client is ISocketStatus; - /// - /// Gets or sets the TimeToError - /// - public int TimeToError { get; set; } + private readonly string PollString; + private readonly Action PollAction; + private readonly long PollTime; - /// - /// Gets or sets the PollString - /// - public string PollString { get; set; } + private Timer PollTimer; - /// - /// Default constructor. Sets pollInterval to 30s, TimeToWarning to 120s, and TimeToError to 300s - /// - public CommunicationMonitorConfig() - { - PollInterval = 30000; - TimeToWarning = 120000; - TimeToError = 300000; - PollString = ""; - } - } + private SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); + + /// + /// GenericCommunicationMonitor constructor + /// + /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected + /// + /// Parent device + /// Communications Client + /// Time in MS for polling + /// Warning time in MS. If a message is not received before this elapsed time the status will be Warning + /// Error time in MS. If a message is not received before this elapsed time the status will be Error + /// string to send for polling + /// Poll time must be less than warning and error time + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, + long warningTime, long errorTime, string pollString) : + base(parent, warningTime, errorTime) + { + if (pollTime > warningTime || pollTime > errorTime) + throw new ArgumentException("pollTime must be less than warning or errorTime"); + + Client = client; + PollTime = pollTime; + PollString = pollString; + + if (IsSocket) + { + (Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange; + } + } + + /// + /// GenericCommunicationMonitor constructor with a bool to specify whether to monitor BytesReceived + /// + /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected + /// + /// Parent device + /// Communications Client + /// Time in MS for polling + /// Warning time in MS. If a message is not received before this elapsed time the status will be Warning + /// Error time in MS. If a message is not received before this elapsed time the status will be Error + /// string to send for polling + /// Use bytesReceived event instead of textReceived when true + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, + long warningTime, long errorTime, string pollString, bool monitorBytesReceived) : + this(parent, client, pollTime, warningTime, errorTime, pollString) + { + MonitorBytesReceived = monitorBytesReceived; + } + + /// + /// GenericCommunicationMonitor constructor with a poll action instead of a poll string + /// + /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected + /// + /// Parent device + /// Communications Client + /// Time in MS for polling + /// Warning time in MS. If a message is not received before this elapsed time the status will be Warning + /// Error time in MS. If a message is not received before this elapsed time the status will be Error + /// Action to execute for polling + /// Poll time must be less than warning and error time + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, + long warningTime, long errorTime, Action pollAction) : + base(parent, warningTime, errorTime) + { + if (pollTime > warningTime || pollTime > errorTime) + throw new ArgumentException("pollTime must be less than warning or errorTime"); + //if (pollTime < 5000) + // throw new ArgumentException("pollTime cannot be less than 5000 ms"); + + Client = client; + PollTime = pollTime; + PollAction = pollAction; + + if (IsSocket) + { + (Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange; + } + + } + + /// + /// GenericCommunicationMonitor constructor with a poll action instead of a poll string and a bool to specify whether to monitor BytesReceived + /// + /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected + /// + /// Parent device + /// Communications Client + /// Time in MS for polling + /// Warning time in MS. If a message is not received before this elapsed time the status will be Warning + /// Error time in MS. If a message is not received before this elapsed time the status will be Error + /// Action to execute for polling + /// Use bytesReceived event instead of textReceived when true + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime, + long warningTime, long errorTime, Action pollAction, bool monitorBytesReceived) : + this(parent, client, pollTime, warningTime, errorTime, pollAction) + { + MonitorBytesReceived = monitorBytesReceived; + } + + + /// + /// GenericCommunicationMonitor constructor with a config object + /// + /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected + /// + /// Parent Device + /// Communications Client + /// Communication Monitor Config object + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, + CommunicationMonitorConfig props) : + this(parent, client, props.PollInterval, props.TimeToWarning, props.TimeToError, props.PollString) + { + if (IsSocket) + { + (Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange; + } + } + + /// + /// GenericCommunicationMonitor constructor with a config object and a bool to specify whether to monitor BytesReceived + /// + /// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected + /// + /// Parent Device + /// Communications Client + /// Communication Monitor Config object + /// Use bytesReceived event instead of textReceived when true + public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, CommunicationMonitorConfig props, bool monitorBytesReceived) : + this(parent, client, props.PollInterval, props.TimeToWarning, props.TimeToError, props.PollString) + { + MonitorBytesReceived = monitorBytesReceived; + } + + /// + /// Start the poll cycle + /// + public override void Start() + { + if (MonitorBytesReceived) + { + Client.BytesReceived -= Client_BytesReceived; + Client.BytesReceived += Client_BytesReceived; + } + else + { + Client.TextReceived -= Client_TextReceived; + Client.TextReceived += Client_TextReceived; + } + + BeginPolling(); + } + + private void Socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + if (!e.Client.IsConnected) + { + // Immediately stop polling and notify that device is offline + Stop(); + Status = MonitorStatus.InError; + ResetErrorTimers(); + } + else + { + // Start polling and set status to unknow and let poll result update the status to IsOk when a response is received + Status = MonitorStatus.StatusUnknown; + Start(); + } + } + + private void BeginPolling() + { + try + { + semaphore.Wait(); + { + if (PollTimer != null) + { + return; + } + + PollTimer = new Timer(o => Poll(), null, 0, PollTime); + } + } + finally + { + semaphore.Release(); + } + } + + /// + /// Stop the poll cycle + /// + public override void Stop() + { + if (MonitorBytesReceived) + { + Client.BytesReceived -= Client_BytesReceived; + } + else + { + Client.TextReceived -= Client_TextReceived; + } + + StopErrorTimers(); + + if (PollTimer == null) + { + return; + } + + PollTimer.Dispose(); + PollTimer = null; + } + + private void Client_TextReceived(object sender, GenericCommMethodReceiveTextArgs e) + { + DataReceived(); + } + + private void Client_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) + { + DataReceived(); + } + + private void DataReceived() + { + Status = MonitorStatus.IsOk; + ResetErrorTimers(); + } + + private void Poll() + { + StartErrorTimers(); + if (Client.IsConnected) + { + //Debug.LogMessage(LogEventLevel.Verbose, this, "Polling"); + if (PollAction != null) + PollAction.Invoke(); + else + Client.SendText(PollString); + } + else + { + this.LogVerbose("Comm not connected"); + } + } +} + +/// +/// Communication Monitor Configuration from Essentials Configuration +/// +public class CommunicationMonitorConfig +{ + /// + /// Gets or sets the PollInterval + /// + public int PollInterval { get; set; } + + /// + /// Gets or sets the TimeToWarning + /// + public int TimeToWarning { get; set; } + + /// + /// Gets or sets the TimeToError + /// + public int TimeToError { get; set; } + + /// + /// Gets or sets the PollString + /// + public string PollString { get; set; } + + /// + /// Default constructor. Sets pollInterval to 30s, TimeToWarning to 120s, and TimeToError to 300s + /// + public CommunicationMonitorConfig() + { + PollInterval = 30000; + TimeToWarning = 120000; + TimeToError = 300000; + PollString = ""; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs b/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs index 7f44fa0f..67ba1047 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs @@ -23,15 +23,15 @@ namespace PepperDash.Essentials.Core /// public event EventHandler StatusChange; - /// - /// Gets or sets the Key - /// - public string Key { get { return Parent.Key + "-comMonitor"; } } + /// + /// Gets or sets the Key + /// + public string Key { get { return Parent.Key + "-comMonitor"; } } - /// - /// Gets or sets the Name - /// - public string Name { get { return "Comm. monitor"; } } + /// + /// Gets or sets the Name + /// + public string Name { get { return "Comm. monitor"; } } /// /// Gets or sets the Parent @@ -59,7 +59,7 @@ namespace PepperDash.Essentials.Core if (value != _Status) { _Status = value; - + OnStatusChange(value); } } @@ -72,12 +72,12 @@ namespace PepperDash.Essentials.Core public string Message { get { return _Message; } - set + set { if (value == _Message) return; _Message = value; OnStatusChange(Status, value); - + } } string _Message; @@ -173,13 +173,13 @@ namespace PepperDash.Essentials.Core /// /// Resets the error timers /// - protected void ResetErrorTimers() - { - if(WarningTimer != null) - WarningTimer.Reset(WarningTime, WarningTime); - if(ErrorTimer != null) - ErrorTimer.Reset(ErrorTime, ErrorTime); + protected void ResetErrorTimers() + { + if (WarningTimer != null) + WarningTimer.Reset(WarningTime, WarningTime); + if (ErrorTimer != null) + ErrorTimer.Reset(ErrorTime, ErrorTime); - } + } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorCollection.cs b/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorCollection.cs index 58aba6a1..a6aa6c3d 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorCollection.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorCollection.cs @@ -11,8 +11,8 @@ using System.ComponentModel; using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Collection class for multiple status monitors /// @@ -166,5 +166,4 @@ namespace PepperDash.Essentials.Core if (handler != null) handler(this, new MonitorStatusChangeEventArgs(status, message)); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs b/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs index 61e70117..d09ec98f 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs @@ -1,6 +1,4 @@ - - -using System; +using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp; @@ -12,1064 +10,1048 @@ using Newtonsoft.Json.Converters; using PepperDash.Essentials.Core.Bridges; using Serilog.Events; -namespace PepperDash.Essentials.Core.Monitoring +namespace PepperDash.Essentials.Core.Monitoring; + +/// +/// Wrapper for the static SystemMonitor class to extend functionality and provide external access +/// to SystemMonitor via APIs +/// +public class SystemMonitorController : EssentialsBridgeableDevice { + private const long UptimePollTime = 300000; + private CTimer _uptimePollTimer; + + private string _uptime; + private string _lastStart; + /// - /// Wrapper for the static SystemMonitor class to extend functionality and provide external access - /// to SystemMonitor via APIs + /// Event fired when any SystemMonitor property changes /// - public class SystemMonitorController : EssentialsBridgeableDevice + public event EventHandler SystemMonitorPropertiesChanged; + + /// + /// Gets or sets the ProgramStatusFeedbackCollection + /// + public Dictionary ProgramStatusFeedbackCollection; + + /// + /// Gets or sets the EthernetStatusFeedbackCollection + /// + public Dictionary EthernetStatusFeedbackCollection; + + /// + /// Gets or sets the TimeZoneFeedback + /// + public IntFeedback TimeZoneFeedback { get; protected set; } + + /// + /// Gets or sets the TimeZoneTextFeedback + /// + public StringFeedback TimeZoneTextFeedback { get; protected set; } + + /// + /// Gets or sets the IoControllerVersionFeedback + /// + public StringFeedback IoControllerVersionFeedback { get; protected set; } + + /// + /// Gets or sets the SnmpVersionFeedback + /// + public StringFeedback SnmpVersionFeedback { get; protected set; } + + /// + /// Gets or sets the BaCnetAppVersionFeedback + /// + public StringFeedback BaCnetAppVersionFeedback { get; protected set; } + + /// + /// Gets or sets the ControllerVersionFeedback + /// + public StringFeedback ControllerVersionFeedback { get; protected set; } + + //new feedbacks. Issue #50 + /// + /// Gets or sets the SerialNumberFeedback + /// + public StringFeedback SerialNumberFeedback { get; protected set; } + + /// + /// Gets or sets the ModelFeedback + /// + public StringFeedback ModelFeedback { get; set; } + + /// + /// Gets or sets the UptimeFeedback + /// + public StringFeedback UptimeFeedback { get; set; } + + /// + /// Gets or sets the LastStartFeedback + /// + public StringFeedback LastStartFeedback { get; set; } + + /// + /// Gets or sets the IsApplianceFeedback + /// + public BoolFeedback IsApplianceFeedback { get; protected set; } + private bool _isApplianceFb { - private const long UptimePollTime = 300000; - private CTimer _uptimePollTimer; + get { return CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; } + } - private string _uptime; - private string _lastStart; + /// + /// Gets or sets the IsServerFeedback + /// + public BoolFeedback IsServerFeedback { get; protected set; } + private bool _isServerFb + { + get { return CrestronEnvironment.DevicePlatform == eDevicePlatform.Server; } + } + + /// + /// Constructor + /// + /// device key + public SystemMonitorController(string key) + : base(key) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Adding SystemMonitorController."); + + SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true; + + TimeZoneFeedback = new IntFeedback(() => SystemMonitor.TimeZoneInformation.TimeZoneNumber); + TimeZoneTextFeedback = new StringFeedback(() => SystemMonitor.TimeZoneInformation.TimeZoneName); + + IoControllerVersionFeedback = new StringFeedback(() => SystemMonitor.VersionInformation.IOPVersion); + SnmpVersionFeedback = new StringFeedback(() => SystemMonitor.VersionInformation.SNMPVersion); + BaCnetAppVersionFeedback = new StringFeedback(() => SystemMonitor.VersionInformation.BACNetVersion); + ControllerVersionFeedback = new StringFeedback(() => SystemMonitor.VersionInformation.ControlSystemVersion); + + SerialNumberFeedback = new StringFeedback(() => CrestronEnvironment.SystemInfo.SerialNumber); + ModelFeedback = new StringFeedback(() => InitialParametersClass.ControllerPromptName); + UptimeFeedback = new StringFeedback(() => _uptime); + LastStartFeedback = new StringFeedback(() => _lastStart); + + IsApplianceFeedback = new BoolFeedback(() => _isApplianceFb); + IsServerFeedback = new BoolFeedback(() => _isServerFb); + + ProgramStatusFeedbackCollection = new Dictionary(); + + foreach (var prog in SystemMonitor.ProgramCollection) + { + var program = new ProgramStatusFeedbacks(prog); + ProgramStatusFeedbackCollection.Add(prog.Number, program); + } + + CreateEthernetStatusFeedbacks(); + UpdateEthernetStatusFeeedbacks(); + + _uptimePollTimer = new CTimer(PollUptime, null, 0, UptimePollTime); + + SystemMonitor.ProgramChange += SystemMonitor_ProgramChange; + SystemMonitor.TimeZoneInformation.TimeZoneChange += TimeZoneInformation_TimeZoneChange; + CrestronEnvironment.EthernetEventHandler += CrestronEnvironmentOnEthernetEventHandler; + CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironmentOnProgramStatusEventHandler; + } + + private void CrestronEnvironmentOnProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType != eProgramStatusEventType.Stopping) return; + + _uptimePollTimer.Stop(); + _uptimePollTimer.Dispose(); + _uptimePollTimer = null; + } + + public void PollUptime(object obj) + { + var consoleResponse = string.Empty; + + CrestronConsole.SendControlSystemCommand("uptime", ref consoleResponse); + + ParseUptime(consoleResponse); + + UptimeFeedback.FireUpdate(); + LastStartFeedback.FireUpdate(); + } + + private void ParseUptime(string response) + { + var splitString = response.Trim().Split('\r', '\n'); + + var lastStartRaw = splitString.FirstOrDefault(o => o.Contains("started")); + var uptimeRaw = splitString.FirstOrDefault(o => o.Contains("running")); + + if (!String.IsNullOrEmpty(lastStartRaw)) + { + var lastStartIndex = lastStartRaw.IndexOf(':'); + _lastStart = lastStartRaw.Substring(lastStartIndex + 1).Trim(); + } + + if (String.IsNullOrEmpty(uptimeRaw)) return; + var forIndex = uptimeRaw.IndexOf("for", StringComparison.Ordinal); + + //4 => "for " to get what's on the right + _uptime = uptimeRaw.Substring(forIndex + 4); + } + + /// + /// ProcessorReboot method + /// + public static void ProcessorReboot() + { + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) return; + Debug.LogMessage(LogEventLevel.Information, "Rebooting..."); + + + var response = string.Empty; + CrestronConsole.SendControlSystemCommand("reboot", ref response); + } + + /// + /// ProgramReset method + /// + public static void ProgramReset(uint index) + { + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) return; + Debug.LogMessage(LogEventLevel.Information, "Resetting Program {0}...", index); + + if (index <= 0 || index > 10) return; + + var cmd = string.Format("progreset -p:{0}", index); + + var response = string.Empty; + CrestronConsole.SendControlSystemCommand(cmd, ref response); + } + + private void CrestronEnvironmentOnEthernetEventHandler(EthernetEventArgs ethernetEventArgs) + { + if (ethernetEventArgs.EthernetEventType != eEthernetEventType.LinkUp) return; + + foreach (var fb in EthernetStatusFeedbackCollection) + { + fb.Value.UpdateEthernetStatus(); + } + } + + private void CreateEthernetStatusFeedbacks() + { + EthernetStatusFeedbackCollection = new Dictionary(); + + Debug.LogMessage(LogEventLevel.Verbose, "Creating {0} EthernetStatusFeedbacks", InitialParametersClass.NumberOfEthernetInterfaces); + + for (short i = 0; i < InitialParametersClass.NumberOfEthernetInterfaces; i++) + { + Debug.LogMessage(LogEventLevel.Verbose, "Creating EthernetStatusFeedback for Interface {0}", i); + var ethernetInterface = new EthernetStatusFeedbacks(i); + EthernetStatusFeedbackCollection.Add(i, ethernetInterface); + } + } + + private void UpdateEthernetStatusFeeedbacks() + { + foreach (var iface in EthernetStatusFeedbackCollection) + { + iface.Value.CurrentIpAddressFeedback.FireUpdate(); + iface.Value.CurrentSubnetMaskFeedback.FireUpdate(); + iface.Value.CurrentDefaultGatewayFeedback.FireUpdate(); + iface.Value.StaticIpAddressFeedback.FireUpdate(); + iface.Value.StaticSubnetMaskFeedback.FireUpdate(); + iface.Value.StaticDefaultGatewayFeedback.FireUpdate(); + iface.Value.HostNameFeedback.FireUpdate(); + iface.Value.DnsServerFeedback.FireUpdate(); + iface.Value.DomainFeedback.FireUpdate(); + iface.Value.DhcpStatusFeedback.FireUpdate(); + iface.Value.MacAddressFeedback.FireUpdate(); + } + } + + /// + /// Gets data in separate thread + /// + private void RefreshSystemMonitorData() + { + // this takes a while, launch a new thread + CrestronInvoke.BeginInvoke(UpdateFeedback); + } + + private void UpdateFeedback(object o) + { + TimeZoneFeedback.FireUpdate(); + TimeZoneTextFeedback.FireUpdate(); + IoControllerVersionFeedback.FireUpdate(); + SnmpVersionFeedback.FireUpdate(); + BaCnetAppVersionFeedback.FireUpdate(); + ControllerVersionFeedback.FireUpdate(); + SerialNumberFeedback.FireUpdate(); + ModelFeedback.FireUpdate(); + + IsApplianceFeedback.FireUpdate(); + IsServerFeedback.FireUpdate(); + + OnSystemMonitorPropertiesChanged(); + } + + private void OnSystemMonitorPropertiesChanged() + { + var handler = SystemMonitorPropertiesChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } + } + + public override bool CustomActivate() + { + RefreshSystemMonitorData(); + + return base.CustomActivate(); + } + + public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + var joinMap = new SystemMonitorJoinMap(joinStart); + + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + + if (bridge != null) + { + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); + } + + Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); + Debug.LogMessage(LogEventLevel.Verbose, this, "Linking API starting at join: {0}", joinStart); + + TimeZoneFeedback.LinkInputSig(trilist.UShortInput[joinMap.TimeZone.JoinNumber]); + TimeZoneTextFeedback.LinkInputSig(trilist.StringInput[joinMap.TimeZoneName.JoinNumber]); + + IoControllerVersionFeedback.LinkInputSig(trilist.StringInput[joinMap.IOControllerVersion.JoinNumber]); + SnmpVersionFeedback.LinkInputSig(trilist.StringInput[joinMap.SnmpAppVersion.JoinNumber]); + BaCnetAppVersionFeedback.LinkInputSig(trilist.StringInput[joinMap.BACnetAppVersion.JoinNumber]); + ControllerVersionFeedback.LinkInputSig(trilist.StringInput[joinMap.ControllerVersion.JoinNumber]); + SerialNumberFeedback.LinkInputSig(trilist.StringInput[joinMap.SerialNumber.JoinNumber]); + ModelFeedback.LinkInputSig(trilist.StringInput[joinMap.Model.JoinNumber]); + UptimeFeedback.LinkInputSig(trilist.StringInput[joinMap.Uptime.JoinNumber]); + LastStartFeedback.LinkInputSig(trilist.StringInput[joinMap.LastBoot.JoinNumber]); + + trilist.SetSigHeldAction(joinMap.ProcessorReboot.JoinNumber, 10000, ProcessorReboot); + + IsApplianceFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsAppliance.JoinNumber]); + IsServerFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsServer.JoinNumber]); + + // iterate the program status feedback collection and map all the joins + LinkProgramInfoJoins(this, trilist, joinMap); + + LinkEthernetInfoJoins(this, trilist, joinMap); + } + + private static void LinkEthernetInfoJoins(SystemMonitorController systemMonitorController, BasicTriList trilist, SystemMonitorJoinMap joinMap) + { + uint ethernetSlotJoinStart = 0; + foreach (var fb in systemMonitorController.EthernetStatusFeedbackCollection) + { + fb.Value.CurrentIpAddressFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.CurrentIpAddress.JoinNumber]); + fb.Value.CurrentSubnetMaskFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.CurrentSubnetMask.JoinNumber]); + fb.Value.CurrentDefaultGatewayFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.CurrentDefaultGateway.JoinNumber]); + fb.Value.StaticIpAddressFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.StaticIpAddress.JoinNumber]); + fb.Value.StaticSubnetMaskFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.StaticSubnetMask.JoinNumber]); + fb.Value.StaticDefaultGatewayFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.StaticDefaultGateway.JoinNumber]); + fb.Value.HostNameFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.HostName.JoinNumber]); + fb.Value.MacAddressFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.MacAddress.JoinNumber]); + fb.Value.DomainFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.Domain.JoinNumber]); + fb.Value.DnsServerFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.DnsServer.JoinNumber]); + fb.Value.DhcpStatusFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.DhcpStatus.JoinNumber]); + + ethernetSlotJoinStart += joinMap.EthernetOffsetJoin.JoinNumber; + } + } + + private static void LinkProgramInfoJoins(SystemMonitorController systemMonitorController, BasicTriList trilist, + SystemMonitorJoinMap joinMap) + { + uint programSlotJoinStart = 0; + + foreach (var p in systemMonitorController.ProgramStatusFeedbackCollection) + { + var programNumber = p.Value.Program.Number; + + trilist.SetBoolSigAction(programSlotJoinStart + joinMap.ProgramStart.JoinNumber, + b => SystemMonitor.ProgramCollection[programNumber].OperatingState = eProgramOperatingState.Start); + p.Value.ProgramStartedFeedback.LinkInputSig(trilist.BooleanInput[programSlotJoinStart + joinMap.ProgramStart.JoinNumber]); + + trilist.SetBoolSigAction(programSlotJoinStart + joinMap.ProgramStop.JoinNumber, + b => SystemMonitor.ProgramCollection[programNumber].OperatingState = eProgramOperatingState.Stop); + p.Value.ProgramStoppedFeedback.LinkInputSig(trilist.BooleanInput[programSlotJoinStart + joinMap.ProgramStop.JoinNumber]); + + trilist.SetBoolSigAction(programSlotJoinStart + joinMap.ProgramRegister.JoinNumber, + b => SystemMonitor.ProgramCollection[programNumber].RegistrationState = eProgramRegistrationState.Register); + p.Value.ProgramRegisteredFeedback.LinkInputSig( + trilist.BooleanInput[programSlotJoinStart + joinMap.ProgramRegister.JoinNumber]); + + trilist.SetBoolSigAction(programSlotJoinStart + joinMap.ProgramUnregister.JoinNumber, + b => SystemMonitor.ProgramCollection[programNumber].RegistrationState = eProgramRegistrationState.Unregister); + p.Value.ProgramUnregisteredFeedback.LinkInputSig( + trilist.BooleanInput[programSlotJoinStart + joinMap.ProgramUnregister.JoinNumber]); + + p.Value.ProgramNameFeedback.LinkInputSig(trilist.StringInput[programSlotJoinStart + joinMap.ProgramName.JoinNumber]); + p.Value.ProgramCompileTimeFeedback.LinkInputSig( + trilist.StringInput[programSlotJoinStart + joinMap.ProgramCompiledTime.JoinNumber]); + p.Value.CrestronDataBaseVersionFeedback.LinkInputSig( + trilist.StringInput[programSlotJoinStart + joinMap.ProgramCrestronDatabaseVersion.JoinNumber]); + p.Value.EnvironmentVersionFeedback.LinkInputSig( + trilist.StringInput[programSlotJoinStart + joinMap.ProgramEnvironmentVersion.JoinNumber]); + p.Value.AggregatedProgramInfoFeedback.LinkInputSig( + trilist.StringInput[programSlotJoinStart + joinMap.AggregatedProgramInfo.JoinNumber]); + + trilist.SetSigHeldAction(programSlotJoinStart + joinMap.ProgramReset.JoinNumber, 10000, () => ProgramReset(programNumber)); + + programSlotJoinStart = programSlotJoinStart + joinMap.ProgramOffsetJoin.JoinSpan; + } + } + + //// Sets the time zone + //public void SetTimeZone(int timeZone) + //{ + // SystemMonitor.TimeZoneInformation.TimeZoneNumber = timeZone; + //} + + /// + /// Responds to program change events and triggers the appropriate feedbacks to update + /// + /// + /// + private void SystemMonitor_ProgramChange(Program sender, ProgramEventArgs args) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Program Change Detected for slot: {0}", sender.Number); + Debug.LogMessage(LogEventLevel.Verbose, this, "Event Type: {0}", args.EventType); + + var program = ProgramStatusFeedbackCollection[sender.Number]; + + switch (args.EventType) + { + case eProgramChangeEventType.OperatingState: + program.ProgramStartedFeedback.FireUpdate(); + program.ProgramStoppedFeedback.FireUpdate(); + program.ProgramInfo.OperatingState = args.OperatingState; + if (args.OperatingState == eProgramOperatingState.Start) + program.GetProgramInfo(); + else + { + program.AggregatedProgramInfoFeedback.FireUpdate(); + program.OnProgramInfoChanged(); + } + break; + case eProgramChangeEventType.RegistrationState: + program.ProgramRegisteredFeedback.FireUpdate(); + program.ProgramUnregisteredFeedback.FireUpdate(); + program.ProgramInfo.RegistrationState = args.RegistrationState; + program.GetProgramInfo(); + break; + } + } + + /// + /// Responds to time zone changes and updates the appropriate feedbacks + /// + /// + private void TimeZoneInformation_TimeZoneChange(TimeZoneEventArgs args) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Time Zone Change Detected."); + TimeZoneFeedback.FireUpdate(); + TimeZoneTextFeedback.FireUpdate(); + + OnSystemMonitorPropertiesChanged(); + } + + + /// + /// Represents an EthernetStatusFeedbacks + /// + public class EthernetStatusFeedbacks + { + /// + /// Gets or sets the HostNameFeedback + /// + public StringFeedback HostNameFeedback { get; protected set; } /// - /// Event fired when any SystemMonitor property changes + /// Gets or sets the DnsServerFeedback /// - public event EventHandler SystemMonitorPropertiesChanged; + public StringFeedback DnsServerFeedback { get; protected set; } /// - /// Gets or sets the ProgramStatusFeedbackCollection + /// Gets or sets the DomainFeedback /// - public Dictionary ProgramStatusFeedbackCollection; + public StringFeedback DomainFeedback { get; protected set; } /// - /// Gets or sets the EthernetStatusFeedbackCollection + /// Gets or sets the MacAddressFeedback /// - public Dictionary EthernetStatusFeedbackCollection; + public StringFeedback MacAddressFeedback { get; protected set; } /// - /// Gets or sets the TimeZoneFeedback + /// Gets or sets the DhcpStatusFeedback /// - public IntFeedback TimeZoneFeedback { get; protected set; } + public StringFeedback DhcpStatusFeedback { get; protected set; } + /// - /// Gets or sets the TimeZoneTextFeedback + /// Gets or sets the CurrentIpAddressFeedback /// - public StringFeedback TimeZoneTextFeedback { get; protected set; } + public StringFeedback CurrentIpAddressFeedback { get; protected set; } /// - /// Gets or sets the IoControllerVersionFeedback + /// Gets or sets the CurrentSubnetMaskFeedback /// - public StringFeedback IoControllerVersionFeedback { get; protected set; } + public StringFeedback CurrentSubnetMaskFeedback { get; protected set; } /// - /// Gets or sets the SnmpVersionFeedback + /// Gets or sets the CurrentDefaultGatewayFeedback /// - public StringFeedback SnmpVersionFeedback { get; protected set; } + public StringFeedback CurrentDefaultGatewayFeedback { get; protected set; } /// - /// Gets or sets the BaCnetAppVersionFeedback + /// Gets or sets the StaticIpAddressFeedback /// - public StringFeedback BaCnetAppVersionFeedback { get; protected set; } + public StringFeedback StaticIpAddressFeedback { get; protected set; } /// - /// Gets or sets the ControllerVersionFeedback + /// Gets or sets the StaticSubnetMaskFeedback /// - public StringFeedback ControllerVersionFeedback { get; protected set; } - - //new feedbacks. Issue #50 - /// - /// Gets or sets the SerialNumberFeedback - /// - public StringFeedback SerialNumberFeedback { get; protected set; } + public StringFeedback StaticSubnetMaskFeedback { get; protected set; } /// - /// Gets or sets the ModelFeedback + /// Gets or sets the StaticDefaultGatewayFeedback /// - public StringFeedback ModelFeedback { get; set; } - - /// - /// Gets or sets the UptimeFeedback - /// - public StringFeedback UptimeFeedback { get; set; } - - /// - /// Gets or sets the LastStartFeedback - /// - public StringFeedback LastStartFeedback { get; set; } - - /// - /// Gets or sets the IsApplianceFeedback - /// - public BoolFeedback IsApplianceFeedback { get; protected set; } - private bool _isApplianceFb - { - get { return CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; } - } - - /// - /// Gets or sets the IsServerFeedback - /// - public BoolFeedback IsServerFeedback { get; protected set; } - private bool _isServerFb - { - get { return CrestronEnvironment.DevicePlatform == eDevicePlatform.Server; } - } + public StringFeedback StaticDefaultGatewayFeedback { get; protected set; } /// /// Constructor /// - /// device key - public SystemMonitorController(string key) - : base(key) + /// index of the ethernet addapter + public EthernetStatusFeedbacks(short adapterIndex) { - Debug.LogMessage(LogEventLevel.Verbose, this, "Adding SystemMonitorController."); - - SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true; - - TimeZoneFeedback = new IntFeedback(() => SystemMonitor.TimeZoneInformation.TimeZoneNumber); - TimeZoneTextFeedback = new StringFeedback(() => SystemMonitor.TimeZoneInformation.TimeZoneName); - - IoControllerVersionFeedback = new StringFeedback(() => SystemMonitor.VersionInformation.IOPVersion); - SnmpVersionFeedback = new StringFeedback(() => SystemMonitor.VersionInformation.SNMPVersion); - BaCnetAppVersionFeedback = new StringFeedback(() => SystemMonitor.VersionInformation.BACNetVersion); - ControllerVersionFeedback = new StringFeedback(() => SystemMonitor.VersionInformation.ControlSystemVersion); - - SerialNumberFeedback = new StringFeedback(() => CrestronEnvironment.SystemInfo.SerialNumber); - ModelFeedback = new StringFeedback(() => InitialParametersClass.ControllerPromptName); - UptimeFeedback = new StringFeedback(() => _uptime); - LastStartFeedback = new StringFeedback(()=> _lastStart); - - IsApplianceFeedback = new BoolFeedback(() => _isApplianceFb); - IsServerFeedback = new BoolFeedback(() => _isServerFb); - - ProgramStatusFeedbackCollection = new Dictionary(); - - foreach (var prog in SystemMonitor.ProgramCollection) - { - var program = new ProgramStatusFeedbacks(prog); - ProgramStatusFeedbackCollection.Add(prog.Number, program); - } - - CreateEthernetStatusFeedbacks(); - UpdateEthernetStatusFeeedbacks(); - - _uptimePollTimer = new CTimer(PollUptime,null,0, UptimePollTime); - - SystemMonitor.ProgramChange += SystemMonitor_ProgramChange; - SystemMonitor.TimeZoneInformation.TimeZoneChange += TimeZoneInformation_TimeZoneChange; - CrestronEnvironment.EthernetEventHandler += CrestronEnvironmentOnEthernetEventHandler; - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironmentOnProgramStatusEventHandler; - } - - private void CrestronEnvironmentOnProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType != eProgramStatusEventType.Stopping) return; - - _uptimePollTimer.Stop(); - _uptimePollTimer.Dispose(); - _uptimePollTimer = null; - } - - /// - /// PollUptime method - /// - public void PollUptime(object obj) - { - var consoleResponse = string.Empty; - - CrestronConsole.SendControlSystemCommand("uptime", ref consoleResponse); - - ParseUptime(consoleResponse); - - UptimeFeedback.FireUpdate(); - LastStartFeedback.FireUpdate(); - } - - private void ParseUptime(string response) - { - var splitString = response.Trim().Split('\r', '\n'); - - var lastStartRaw = splitString.FirstOrDefault(o => o.Contains("started")); - var uptimeRaw = splitString.FirstOrDefault(o => o.Contains("running")); - - if (!String.IsNullOrEmpty(lastStartRaw)) - { - var lastStartIndex = lastStartRaw.IndexOf(':'); - _lastStart = lastStartRaw.Substring(lastStartIndex + 1).Trim(); - } - - if (String.IsNullOrEmpty(uptimeRaw)) return; - var forIndex = uptimeRaw.IndexOf("for", StringComparison.Ordinal); - - //4 => "for " to get what's on the right - _uptime = uptimeRaw.Substring(forIndex + 4); - } - - /// - /// ProcessorReboot method - /// - public static void ProcessorReboot() - { - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) return; - Debug.LogMessage(LogEventLevel.Information, "Rebooting..."); - - - var response = string.Empty; - CrestronConsole.SendControlSystemCommand("reboot", ref response); - } - - /// - /// ProgramReset method - /// - public static void ProgramReset(uint index) - { - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) return; - Debug.LogMessage(LogEventLevel.Information, "Resetting Program {0}...", index); - - if (index <= 0 || index > 10) return; - - var cmd = string.Format("progreset -p:{0}", index); - - var response = string.Empty; - CrestronConsole.SendControlSystemCommand(cmd, ref response); - } - - private void CrestronEnvironmentOnEthernetEventHandler(EthernetEventArgs ethernetEventArgs) - { - if (ethernetEventArgs.EthernetEventType != eEthernetEventType.LinkUp) return; - - foreach (var fb in EthernetStatusFeedbackCollection) - { - fb.Value.UpdateEthernetStatus(); - } - } - - private void CreateEthernetStatusFeedbacks() - { - EthernetStatusFeedbackCollection = new Dictionary(); - - Debug.LogMessage(LogEventLevel.Verbose, "Creating {0} EthernetStatusFeedbacks", InitialParametersClass.NumberOfEthernetInterfaces); - - for (short i = 0; i < InitialParametersClass.NumberOfEthernetInterfaces; i++) - { - Debug.LogMessage(LogEventLevel.Verbose, "Creating EthernetStatusFeedback for Interface {0}", i); - var ethernetInterface = new EthernetStatusFeedbacks(i); - EthernetStatusFeedbackCollection.Add(i, ethernetInterface); - } - } - - private void UpdateEthernetStatusFeeedbacks() - { - foreach (var iface in EthernetStatusFeedbackCollection) - { - iface.Value.CurrentIpAddressFeedback.FireUpdate(); - iface.Value.CurrentSubnetMaskFeedback.FireUpdate(); - iface.Value.CurrentDefaultGatewayFeedback.FireUpdate(); - iface.Value.StaticIpAddressFeedback.FireUpdate(); - iface.Value.StaticSubnetMaskFeedback.FireUpdate(); - iface.Value.StaticDefaultGatewayFeedback.FireUpdate(); - iface.Value.HostNameFeedback.FireUpdate(); - iface.Value.DnsServerFeedback.FireUpdate(); - iface.Value.DomainFeedback.FireUpdate(); - iface.Value.DhcpStatusFeedback.FireUpdate(); - iface.Value.MacAddressFeedback.FireUpdate(); - } - } - - /// - /// Gets data in separate thread - /// - private void RefreshSystemMonitorData() - { - // this takes a while, launch a new thread - CrestronInvoke.BeginInvoke(UpdateFeedback); - } - - private void UpdateFeedback(object o) - { - TimeZoneFeedback.FireUpdate(); - TimeZoneTextFeedback.FireUpdate(); - IoControllerVersionFeedback.FireUpdate(); - SnmpVersionFeedback.FireUpdate(); - BaCnetAppVersionFeedback.FireUpdate(); - ControllerVersionFeedback.FireUpdate(); - SerialNumberFeedback.FireUpdate(); - ModelFeedback.FireUpdate(); - - IsApplianceFeedback.FireUpdate(); - IsServerFeedback.FireUpdate(); - - OnSystemMonitorPropertiesChanged(); - } - - private void OnSystemMonitorPropertiesChanged() - { - var handler = SystemMonitorPropertiesChanged; - if (handler != null) - { - handler(this, new EventArgs()); - } - } - - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() - { - RefreshSystemMonitorData(); - - return base.CustomActivate(); - } - - /// - /// LinkToApi method - /// - /// - public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - var joinMap = new SystemMonitorJoinMap(joinStart); - - var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); - - if (!string.IsNullOrEmpty(joinMapSerialized)) - joinMap = JsonConvert.DeserializeObject(joinMapSerialized); - - if (bridge != null) - { - bridge.AddJoinMap(Key, joinMap); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); - } - - Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); - Debug.LogMessage(LogEventLevel.Verbose, this, "Linking API starting at join: {0}", joinStart); - - TimeZoneFeedback.LinkInputSig(trilist.UShortInput[joinMap.TimeZone.JoinNumber]); - TimeZoneTextFeedback.LinkInputSig(trilist.StringInput[joinMap.TimeZoneName.JoinNumber]); - - IoControllerVersionFeedback.LinkInputSig(trilist.StringInput[joinMap.IOControllerVersion.JoinNumber]); - SnmpVersionFeedback.LinkInputSig(trilist.StringInput[joinMap.SnmpAppVersion.JoinNumber]); - BaCnetAppVersionFeedback.LinkInputSig(trilist.StringInput[joinMap.BACnetAppVersion.JoinNumber]); - ControllerVersionFeedback.LinkInputSig(trilist.StringInput[joinMap.ControllerVersion.JoinNumber]); - SerialNumberFeedback.LinkInputSig(trilist.StringInput[joinMap.SerialNumber.JoinNumber]); - ModelFeedback.LinkInputSig(trilist.StringInput[joinMap.Model.JoinNumber]); - UptimeFeedback.LinkInputSig(trilist.StringInput[joinMap.Uptime.JoinNumber]); - LastStartFeedback.LinkInputSig(trilist.StringInput[joinMap.LastBoot.JoinNumber]); - - trilist.SetSigHeldAction(joinMap.ProcessorReboot.JoinNumber, 10000, ProcessorReboot); - - IsApplianceFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsAppliance.JoinNumber]); - IsServerFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsServer.JoinNumber]); - - // iterate the program status feedback collection and map all the joins - LinkProgramInfoJoins(this, trilist, joinMap); - - LinkEthernetInfoJoins(this, trilist, joinMap); - } - - private static void LinkEthernetInfoJoins(SystemMonitorController systemMonitorController, BasicTriList trilist, SystemMonitorJoinMap joinMap) - { - uint ethernetSlotJoinStart = 0; - foreach (var fb in systemMonitorController.EthernetStatusFeedbackCollection) - { - fb.Value.CurrentIpAddressFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.CurrentIpAddress.JoinNumber]); - fb.Value.CurrentSubnetMaskFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.CurrentSubnetMask.JoinNumber]); - fb.Value.CurrentDefaultGatewayFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.CurrentDefaultGateway.JoinNumber]); - fb.Value.StaticIpAddressFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.StaticIpAddress.JoinNumber]); - fb.Value.StaticSubnetMaskFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.StaticSubnetMask.JoinNumber]); - fb.Value.StaticDefaultGatewayFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.StaticDefaultGateway.JoinNumber]); - fb.Value.HostNameFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.HostName.JoinNumber]); - fb.Value.MacAddressFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.MacAddress.JoinNumber]); - fb.Value.DomainFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.Domain.JoinNumber]); - fb.Value.DnsServerFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.DnsServer.JoinNumber]); - fb.Value.DhcpStatusFeedback.LinkInputSig(trilist.StringInput[ethernetSlotJoinStart + joinMap.DhcpStatus.JoinNumber]); - - ethernetSlotJoinStart += joinMap.EthernetOffsetJoin.JoinNumber; - } - } - - private static void LinkProgramInfoJoins(SystemMonitorController systemMonitorController, BasicTriList trilist, - SystemMonitorJoinMap joinMap) - { - uint programSlotJoinStart = 0; - - foreach (var p in systemMonitorController.ProgramStatusFeedbackCollection) - { - var programNumber = p.Value.Program.Number; - - trilist.SetBoolSigAction(programSlotJoinStart + joinMap.ProgramStart.JoinNumber, - b => SystemMonitor.ProgramCollection[programNumber].OperatingState = eProgramOperatingState.Start); - p.Value.ProgramStartedFeedback.LinkInputSig(trilist.BooleanInput[programSlotJoinStart + joinMap.ProgramStart.JoinNumber]); - - trilist.SetBoolSigAction(programSlotJoinStart + joinMap.ProgramStop.JoinNumber, - b => SystemMonitor.ProgramCollection[programNumber].OperatingState = eProgramOperatingState.Stop); - p.Value.ProgramStoppedFeedback.LinkInputSig(trilist.BooleanInput[programSlotJoinStart + joinMap.ProgramStop.JoinNumber]); - - trilist.SetBoolSigAction(programSlotJoinStart + joinMap.ProgramRegister.JoinNumber, - b => SystemMonitor.ProgramCollection[programNumber].RegistrationState = eProgramRegistrationState.Register); - p.Value.ProgramRegisteredFeedback.LinkInputSig( - trilist.BooleanInput[programSlotJoinStart + joinMap.ProgramRegister.JoinNumber]); - - trilist.SetBoolSigAction(programSlotJoinStart + joinMap.ProgramUnregister.JoinNumber, - b => SystemMonitor.ProgramCollection[programNumber].RegistrationState = eProgramRegistrationState.Unregister); - p.Value.ProgramUnregisteredFeedback.LinkInputSig( - trilist.BooleanInput[programSlotJoinStart + joinMap.ProgramUnregister.JoinNumber]); - - p.Value.ProgramNameFeedback.LinkInputSig(trilist.StringInput[programSlotJoinStart + joinMap.ProgramName.JoinNumber]); - p.Value.ProgramCompileTimeFeedback.LinkInputSig( - trilist.StringInput[programSlotJoinStart + joinMap.ProgramCompiledTime.JoinNumber]); - p.Value.CrestronDataBaseVersionFeedback.LinkInputSig( - trilist.StringInput[programSlotJoinStart + joinMap.ProgramCrestronDatabaseVersion.JoinNumber]); - p.Value.EnvironmentVersionFeedback.LinkInputSig( - trilist.StringInput[programSlotJoinStart + joinMap.ProgramEnvironmentVersion.JoinNumber]); - p.Value.AggregatedProgramInfoFeedback.LinkInputSig( - trilist.StringInput[programSlotJoinStart + joinMap.AggregatedProgramInfo.JoinNumber]); - - trilist.SetSigHeldAction(programSlotJoinStart + joinMap.ProgramReset.JoinNumber, 10000, () => ProgramReset(programNumber)); - - programSlotJoinStart = programSlotJoinStart + joinMap.ProgramOffsetJoin.JoinSpan; - } - } - - //// Sets the time zone - //public void SetTimeZone(int timeZone) - //{ - // SystemMonitor.TimeZoneInformation.TimeZoneNumber = timeZone; - //} - - /// - /// Responds to program change events and triggers the appropriate feedbacks to update - /// - /// - /// - private void SystemMonitor_ProgramChange(Program sender, ProgramEventArgs args) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Program Change Detected for slot: {0}", sender.Number); - Debug.LogMessage(LogEventLevel.Verbose, this, "Event Type: {0}", args.EventType); - - var program = ProgramStatusFeedbackCollection[sender.Number]; - - switch (args.EventType) - { - case eProgramChangeEventType.OperatingState: - program.ProgramStartedFeedback.FireUpdate(); - program.ProgramStoppedFeedback.FireUpdate(); - program.ProgramInfo.OperatingState = args.OperatingState; - if (args.OperatingState == eProgramOperatingState.Start) - program.GetProgramInfo(); - else - { - program.AggregatedProgramInfoFeedback.FireUpdate(); - program.OnProgramInfoChanged(); - } - break; - case eProgramChangeEventType.RegistrationState: - program.ProgramRegisteredFeedback.FireUpdate(); - program.ProgramUnregisteredFeedback.FireUpdate(); - program.ProgramInfo.RegistrationState = args.RegistrationState; - program.GetProgramInfo(); - break; - } - } - - /// - /// Responds to time zone changes and updates the appropriate feedbacks - /// - /// - private void TimeZoneInformation_TimeZoneChange(TimeZoneEventArgs args) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Time Zone Change Detected."); - TimeZoneFeedback.FireUpdate(); - TimeZoneTextFeedback.FireUpdate(); - - OnSystemMonitorPropertiesChanged(); - } - - /// - /// Represents an EthernetStatusFeedbacks - /// - public class EthernetStatusFeedbacks - { - /// - /// Gets or sets the HostNameFeedback - /// - public StringFeedback HostNameFeedback { get; protected set; } - - /// - /// Gets or sets the DnsServerFeedback - /// - public StringFeedback DnsServerFeedback { get; protected set; } - - /// - /// Gets or sets the DomainFeedback - /// - public StringFeedback DomainFeedback { get; protected set; } - - /// - /// Gets or sets the MacAddressFeedback - /// - public StringFeedback MacAddressFeedback { get; protected set; } - - /// - /// Gets or sets the DhcpStatusFeedback - /// - public StringFeedback DhcpStatusFeedback { get; protected set; } - - - /// - /// Gets or sets the CurrentIpAddressFeedback - /// - public StringFeedback CurrentIpAddressFeedback { get; protected set; } - - /// - /// Gets or sets the CurrentSubnetMaskFeedback - /// - public StringFeedback CurrentSubnetMaskFeedback { get; protected set; } - - /// - /// Gets or sets the CurrentDefaultGatewayFeedback - /// - public StringFeedback CurrentDefaultGatewayFeedback { get; protected set; } - - /// - /// Gets or sets the StaticIpAddressFeedback - /// - public StringFeedback StaticIpAddressFeedback { get; protected set; } - - /// - /// Gets or sets the StaticSubnetMaskFeedback - /// - public StringFeedback StaticSubnetMaskFeedback { get; protected set; } - - /// - /// Gets or sets the StaticDefaultGatewayFeedback - /// - public StringFeedback StaticDefaultGatewayFeedback { get; protected set; } - - /// - /// Constructor - /// - /// index of the ethernet addapter - public EthernetStatusFeedbacks(short adapterIndex) - { - Debug.LogMessage(LogEventLevel.Verbose, "Ethernet Information for interface {0}", adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Hostname: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Current IP Address: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Current Subnet Mask: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Current Router: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Static IP Address: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_STATIC_IPADDRESS, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Static Subnet Mask: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_STATIC_IPMASK, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Static Router: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_STATIC_ROUTER, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} DNS Servers: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} DHCP State: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Domain Name: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, adapterIndex), adapterIndex); - Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} MAC Address: {0}", CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, adapterIndex), adapterIndex); - HostNameFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, adapterIndex)); - - CurrentIpAddressFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterIndex)); - CurrentDefaultGatewayFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterIndex)); - CurrentSubnetMaskFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterIndex)); - StaticIpAddressFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterIndex)); - StaticDefaultGatewayFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterIndex)); - StaticSubnetMaskFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterIndex)); - DomainFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, adapterIndex)); - DnsServerFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER, adapterIndex)); - MacAddressFeedback = - new StringFeedback( - () => - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, adapterIndex)); - - DhcpStatusFeedback = new StringFeedback( + Debug.LogMessage(LogEventLevel.Verbose, "Ethernet Information for interface {0}", adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Hostname: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Current IP Address: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Current Subnet Mask: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Current Router: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Static IP Address: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_STATIC_IPADDRESS, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Static Subnet Mask: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_STATIC_IPMASK, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Static Router: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_STATIC_ROUTER, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} DNS Servers: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} DHCP State: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} Domain Name: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, adapterIndex), adapterIndex); + Debug.LogMessage(LogEventLevel.Verbose, "Adapter Index: {1} MAC Address: {0}", CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, adapterIndex), adapterIndex); + HostNameFeedback = + new StringFeedback( () => CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, adapterIndex)); - } + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, adapterIndex)); - /// - /// UpdateEthernetStatus method - /// - public void UpdateEthernetStatus() + CurrentIpAddressFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterIndex)); + CurrentDefaultGatewayFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterIndex)); + CurrentSubnetMaskFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterIndex)); + StaticIpAddressFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterIndex)); + StaticDefaultGatewayFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterIndex)); + StaticSubnetMaskFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterIndex)); + DomainFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, adapterIndex)); + DnsServerFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER, adapterIndex)); + MacAddressFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, adapterIndex)); + + DhcpStatusFeedback = new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, adapterIndex)); + + CurrentIpAddressFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterIndex)); + CurrentDefaultGatewayFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterIndex)); + CurrentSubnetMaskFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterIndex)); + StaticIpAddressFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterIndex)); + StaticDefaultGatewayFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterIndex)); + StaticSubnetMaskFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterIndex)); + DomainFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, adapterIndex)); + DnsServerFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER, adapterIndex)); + MacAddressFeedback = + new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, adapterIndex)); + + DhcpStatusFeedback = new StringFeedback( + () => + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, adapterIndex)); + } + + public void UpdateEthernetStatus() + { + HostNameFeedback.FireUpdate(); + CurrentIpAddressFeedback.FireUpdate(); + CurrentSubnetMaskFeedback.FireUpdate(); + CurrentDefaultGatewayFeedback.FireUpdate(); + StaticIpAddressFeedback.FireUpdate(); + StaticSubnetMaskFeedback.FireUpdate(); + StaticDefaultGatewayFeedback.FireUpdate(); + DomainFeedback.FireUpdate(); + DnsServerFeedback.FireUpdate(); + MacAddressFeedback.FireUpdate(); + DhcpStatusFeedback.FireUpdate(); + } + } +} + +public class ProgramStatusFeedbacks +{ + public event EventHandler ProgramInfoChanged; + + public Program Program; + + public ProgramInfo ProgramInfo { get; set; } + + public BoolFeedback ProgramStartedFeedback; + public BoolFeedback ProgramStoppedFeedback; + public BoolFeedback ProgramRegisteredFeedback; + public BoolFeedback ProgramUnregisteredFeedback; + + public StringFeedback ProgramNameFeedback; + public StringFeedback ProgramCompileTimeFeedback; + public StringFeedback CrestronDataBaseVersionFeedback; + // SIMPL windows version + public StringFeedback EnvironmentVersionFeedback; + public StringFeedback AggregatedProgramInfoFeedback; + + public ProgramStatusFeedbacks(Program program) + { + ProgramInfo = new ProgramInfo(program.Number); + + Program = program; + + ProgramInfo.OperatingState = Program.OperatingState; + ProgramInfo.RegistrationState = Program.RegistrationState; + + ProgramStartedFeedback = new BoolFeedback(() => Program.OperatingState == eProgramOperatingState.Start); + ProgramStartedFeedback.FireUpdate(); + + ProgramStoppedFeedback = new BoolFeedback(() => Program.OperatingState == eProgramOperatingState.Stop); + ProgramStoppedFeedback.FireUpdate(); + + ProgramRegisteredFeedback = + new BoolFeedback(() => Program.RegistrationState == eProgramRegistrationState.Register); + ProgramRegisteredFeedback.FireUpdate(); + + ProgramUnregisteredFeedback = + new BoolFeedback(() => Program.RegistrationState == eProgramRegistrationState.Unregister); + ProgramUnregisteredFeedback.FireUpdate(); + + ProgramNameFeedback = new StringFeedback(() => ProgramInfo.ProgramFile); + CrestronDataBaseVersionFeedback = new StringFeedback(() => ProgramInfo.CrestronDb); + EnvironmentVersionFeedback = new StringFeedback(() => ProgramInfo.Environment); + ProgramCompileTimeFeedback = new StringFeedback(() => ProgramInfo.CompileTime); + AggregatedProgramInfoFeedback = new StringFeedback(() => JsonConvert.SerializeObject(ProgramInfo)); + + GetProgramInfo(); + } + + /// + /// Retrieves information about a running program + /// + public void GetProgramInfo() + { + CrestronInvoke.BeginInvoke(GetProgramInfo); + } + + private void GetProgramInfo(object o) + { + Debug.LogMessage(LogEventLevel.Verbose, "Attempting to get program info for slot: {0}", Program.Number); + + string response = null; + + if (Program.RegistrationState == eProgramRegistrationState.Unregister || Program.OperatingState == eProgramOperatingState.Stop) + { + Debug.LogMessage(LogEventLevel.Verbose, "Program {0} not registered. Setting default values for program information.", + Program.Number); + + ProgramInfo = new ProgramInfo(Program.Number) { - HostNameFeedback.FireUpdate(); - CurrentIpAddressFeedback.FireUpdate(); - CurrentSubnetMaskFeedback.FireUpdate(); - CurrentDefaultGatewayFeedback.FireUpdate(); - StaticIpAddressFeedback.FireUpdate(); - StaticSubnetMaskFeedback.FireUpdate(); - StaticDefaultGatewayFeedback.FireUpdate(); - DomainFeedback.FireUpdate(); - DnsServerFeedback.FireUpdate(); - MacAddressFeedback.FireUpdate(); - DhcpStatusFeedback.FireUpdate(); - } + OperatingState = Program.OperatingState, + RegistrationState = Program.RegistrationState + }; + + return; + } + + var success = CrestronConsole.SendControlSystemCommand( + string.Format("progcomments:{0}", Program.Number), ref response); + + if (!success) + { + Debug.LogMessage(LogEventLevel.Verbose, "Progcomments Attempt Unsuccessful for slot: {0}", Program.Number); + UpdateFeedbacks(); + return; + } + + if (response.ToLower().Contains("bad or incomplete")) + { + Debug.LogMessage(LogEventLevel.Verbose, + "Program in slot {0} not running. Setting default ProgramInfo for slot: {0}", + Program.Number); + + // Assume no valid program info. Constructing a new object will wipe all properties + ProgramInfo = new ProgramInfo(Program.Number) + { + OperatingState = Program.OperatingState, + RegistrationState = Program.RegistrationState + }; + + UpdateFeedbacks(); + + return; } - /// - /// Represents a ProgramStatusFeedbacks - /// - public class ProgramStatusFeedbacks + // Shared properteis + ProgramInfo.ProgramFile = ParseConsoleData(response, "Program File", ": ", "\n"); + ProgramInfo.CompilerRevision = ParseConsoleData(response, "Compiler Rev", ": ", "\n"); + ProgramInfo.CompileTime = ParseConsoleData(response, "Compiled On", ": ", "\n"); + ProgramInfo.Include4Dat = ParseConsoleData(response, "Include4.dat", ": ", "\n"); + + + if (ProgramInfo.ProgramFile.Contains(".dll")) { - /// - /// Event fired when ProgramInfo changes - /// - public event EventHandler ProgramInfoChanged; + // SSP Program + ProgramInfo.FriendlyName = ParseConsoleData(response, "Friendly Name", ": ", "\n"); + ProgramInfo.ApplicationName = ParseConsoleData(response, "Application Name", ": ", "\n"); + ProgramInfo.ProgramTool = ParseConsoleData(response, "Program Tool", ": ", "\n"); + ProgramInfo.MinFirmwareVersion = ParseConsoleData(response, "Min Firmware Version", ": ", + "\n"); + ProgramInfo.PlugInVersion = ParseConsoleData(response, "PlugInVersion", ": ", "\n"); - /// - /// Gets or sets the Program - /// - public Program Program; + ProgramInfo.ProgramFile += string.Format(" {0}.{1}.{2}", + ProgramInfo.CompilerRevisionInfo.Major, + ProgramInfo.CompilerRevisionInfo.Minor, + ProgramInfo.CompilerRevisionInfo.Build); - /// - /// Gets or sets the ProgramInfo - /// - public ProgramInfo ProgramInfo { get; set; } + ProgramInfo.Environment = ProgramInfo.ProgramTool; + } + else if (ProgramInfo.ProgramFile.Contains(".smw")) + { + // SIMPL Windows Program + ProgramInfo.FriendlyName = ParseConsoleData(response, "Friendly Name", ":", "\n"); + ProgramInfo.SystemName = ParseConsoleData(response, "System Name", ": ", "\n"); + ProgramInfo.CrestronDb = ParseConsoleData(response, "CrestronDB", ": ", "\n"); + ProgramInfo.Environment = ParseConsoleData(response, "Source Env", ": ", "\n"); + ProgramInfo.Programmer = ParseConsoleData(response, "Programmer", ": ", "\n"); + } + Debug.LogMessage(LogEventLevel.Verbose, "Program info for slot {0} successfully updated", Program.Number); - /// - /// Gets or sets the ProgramStartedFeedback - /// - public BoolFeedback ProgramStartedFeedback; + UpdateFeedbacks(); + } - /// - /// Gets or sets the ProgramStoppedFeedback - /// - public BoolFeedback ProgramStoppedFeedback; + private void UpdateFeedbacks() + { + ProgramNameFeedback.FireUpdate(); + ProgramCompileTimeFeedback.FireUpdate(); + CrestronDataBaseVersionFeedback.FireUpdate(); + EnvironmentVersionFeedback.FireUpdate(); - /// - /// Gets or sets the ProgramRegisteredFeedback - /// - public BoolFeedback ProgramRegisteredFeedback; + AggregatedProgramInfoFeedback.FireUpdate(); - /// - /// Gets or sets the ProgramUnregisteredFeedback - /// - public BoolFeedback ProgramUnregisteredFeedback; + OnProgramInfoChanged(); + } - /// - /// Gets or sets the ProgramNameFeedback - /// - public StringFeedback ProgramNameFeedback; + public void OnProgramInfoChanged() + { + //Debug.LogMessage(LogEventLevel.Debug, "Firing ProgramInfoChanged for slot: {0}", Program.Number); + var handler = ProgramInfoChanged; + if (handler != null) + { + handler(this, new ProgramInfoEventArgs(ProgramInfo)); + } + } - /// - /// Gets or sets the ProgramCompileTimeFeedback - /// - public StringFeedback ProgramCompileTimeFeedback; + private string ParseConsoleData(string data, string line, string startString, string endString) + { + var outputData = ""; - /// - /// Gets or sets the CrestronDataBaseVersionFeedback - /// - public StringFeedback CrestronDataBaseVersionFeedback; + if (data.Length <= 0) return outputData; - // SIMPL windows version - /// - /// Gets or sets the EnvironmentVersionFeedback - /// - public StringFeedback EnvironmentVersionFeedback; + if (!data.Contains(line)) + { + return outputData; + } - /// - /// Gets or sets the AggregatedProgramInfoFeedback - /// - public StringFeedback AggregatedProgramInfoFeedback; + try + { + //Debug.LogMessage(LogEventLevel.Verbose, "ParseConsoleData Data: {0}, Line {1}, startStirng {2}, endString {3}", data, line, startString, endString); + var linePosition = data.IndexOf(line, StringComparison.Ordinal); + var startPosition = data.IndexOf(startString, linePosition, StringComparison.Ordinal) + + startString.Length; + var endPosition = data.IndexOf(endString, startPosition, StringComparison.Ordinal); + outputData = data.Substring(startPosition, endPosition - startPosition).Trim(); + //Debug.LogMessage(LogEventLevel.Verbose, "ParseConsoleData Return: {0}", outputData); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Error, "Error Parsing Console Data: {0}", e); + Debug.LogMessage(LogEventLevel.Verbose, "Stack Trace: {stackTrace}", e.StackTrace); + } - /// - /// Constructor - /// - /// program to get status about - public ProgramStatusFeedbacks(Program program) - { - ProgramInfo = new ProgramInfo(program.Number); - - Program = program; - - ProgramInfo.OperatingState = Program.OperatingState; - ProgramInfo.RegistrationState = Program.RegistrationState; - - ProgramStartedFeedback = new BoolFeedback(() => Program.OperatingState == eProgramOperatingState.Start); - ProgramStartedFeedback.FireUpdate(); - - ProgramStoppedFeedback = new BoolFeedback(() => Program.OperatingState == eProgramOperatingState.Stop); - ProgramStoppedFeedback.FireUpdate(); - - ProgramRegisteredFeedback = - new BoolFeedback(() => Program.RegistrationState == eProgramRegistrationState.Register); - ProgramRegisteredFeedback.FireUpdate(); - - ProgramUnregisteredFeedback = - new BoolFeedback(() => Program.RegistrationState == eProgramRegistrationState.Unregister); - ProgramUnregisteredFeedback.FireUpdate(); - - ProgramNameFeedback = new StringFeedback(() => ProgramInfo.ProgramFile); - CrestronDataBaseVersionFeedback = new StringFeedback(() => ProgramInfo.CrestronDb); - EnvironmentVersionFeedback = new StringFeedback(() => ProgramInfo.Environment); - ProgramCompileTimeFeedback = new StringFeedback(() => ProgramInfo.CompileTime); - AggregatedProgramInfoFeedback = new StringFeedback(() => JsonConvert.SerializeObject(ProgramInfo)); - - GetProgramInfo(); - } - - /// - /// GetProgramInfo method - /// - public void GetProgramInfo() - { - CrestronInvoke.BeginInvoke(GetProgramInfo); - } - - private void GetProgramInfo(object o) - { - Debug.LogMessage(LogEventLevel.Verbose, "Attempting to get program info for slot: {0}", Program.Number); - - string response = null; - - if (Program.RegistrationState == eProgramRegistrationState.Unregister || Program.OperatingState == eProgramOperatingState.Stop) - { - Debug.LogMessage(LogEventLevel.Verbose, "Program {0} not registered. Setting default values for program information.", - Program.Number); - - ProgramInfo = new ProgramInfo(Program.Number) - { - OperatingState = Program.OperatingState, - RegistrationState = Program.RegistrationState - }; - - return; - } - - var success = CrestronConsole.SendControlSystemCommand( - string.Format("progcomments:{0}", Program.Number), ref response); - - if (!success) - { - Debug.LogMessage(LogEventLevel.Verbose, "Progcomments Attempt Unsuccessful for slot: {0}", Program.Number); - UpdateFeedbacks(); - return; - } - - if (response.ToLower().Contains("bad or incomplete")) - { - Debug.LogMessage(LogEventLevel.Verbose, - "Program in slot {0} not running. Setting default ProgramInfo for slot: {0}", - Program.Number); - - // Assume no valid program info. Constructing a new object will wipe all properties - ProgramInfo = new ProgramInfo(Program.Number) - { - OperatingState = Program.OperatingState, - RegistrationState = Program.RegistrationState - }; - - UpdateFeedbacks(); - - return; - } + return outputData; + } +} - // Shared properteis - ProgramInfo.ProgramFile = ParseConsoleData(response, "Program File", ": ", "\n"); - ProgramInfo.CompilerRevision = ParseConsoleData(response, "Compiler Rev", ": ", "\n"); - ProgramInfo.CompileTime = ParseConsoleData(response, "Compiled On", ": ", "\n"); - ProgramInfo.Include4Dat = ParseConsoleData(response, "Include4.dat", ": ", "\n"); +/// +/// Represents a ProgramInfo +/// +public class ProgramInfo +{ + // Shared properties + /// + /// Gets the ProgramNumber + /// + [JsonProperty("programNumber")] + public uint ProgramNumber { get; private set; } - if (ProgramInfo.ProgramFile.Contains(".dll")) - { - // SSP Program - ProgramInfo.FriendlyName = ParseConsoleData(response, "Friendly Name", ": ", "\n"); - ProgramInfo.ApplicationName = ParseConsoleData(response, "Application Name", ": ", "\n"); - ProgramInfo.ProgramTool = ParseConsoleData(response, "Program Tool", ": ", "\n"); - ProgramInfo.MinFirmwareVersion = ParseConsoleData(response, "Min Firmware Version", ": ", - "\n"); - ProgramInfo.PlugInVersion = ParseConsoleData(response, "PlugInVersion", ": ", "\n"); + /// + /// Gets or sets the OperatingState + /// + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("operatingState")] + public eProgramOperatingState OperatingState { get; set; } - ProgramInfo.ProgramFile += string.Format(" {0}.{1}.{2}", - ProgramInfo.CompilerRevisionInfo.Major, - ProgramInfo.CompilerRevisionInfo.Minor, - ProgramInfo.CompilerRevisionInfo.Build); + /// + /// Gets or sets the RegistrationState + /// + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("registrationState")] + public eProgramRegistrationState RegistrationState { get; set; } - ProgramInfo.Environment = ProgramInfo.ProgramTool; - } - else if (ProgramInfo.ProgramFile.Contains(".smw")) - { - // SIMPL Windows Program - ProgramInfo.FriendlyName = ParseConsoleData(response, "Friendly Name", ":", "\n"); - ProgramInfo.SystemName = ParseConsoleData(response, "System Name", ": ", "\n"); - ProgramInfo.CrestronDb = ParseConsoleData(response, "CrestronDB", ": ", "\n"); - ProgramInfo.Environment = ParseConsoleData(response, "Source Env", ": ", "\n"); - ProgramInfo.Programmer = ParseConsoleData(response, "Programmer", ": ", "\n"); - } - Debug.LogMessage(LogEventLevel.Verbose, "Program info for slot {0} successfully updated", Program.Number); + /// + /// Gets or sets the ProgramFile + /// + [JsonProperty("programFile")] + public string ProgramFile { get; set; } - UpdateFeedbacks(); - } + /// + /// Gets or sets the FriendlyName + /// + [JsonProperty("friendlyName")] + public string FriendlyName { get; set; } - private void UpdateFeedbacks() - { - ProgramNameFeedback.FireUpdate(); - ProgramCompileTimeFeedback.FireUpdate(); - CrestronDataBaseVersionFeedback.FireUpdate(); - EnvironmentVersionFeedback.FireUpdate(); + /// + /// Gets or sets the CompilerRevision + /// + [JsonProperty("compilerRevision")] + public string CompilerRevision { get; set; } - AggregatedProgramInfoFeedback.FireUpdate(); - - OnProgramInfoChanged(); - } - - /// - /// OnProgramInfoChanged method - /// - public void OnProgramInfoChanged() - { - //Debug.LogMessage(LogEventLevel.Debug, "Firing ProgramInfoChanged for slot: {0}", Program.Number); - var handler = ProgramInfoChanged; - if (handler != null) - { - handler(this, new ProgramInfoEventArgs(ProgramInfo)); - } - } - - private string ParseConsoleData(string data, string line, string startString, string endString) - { - var outputData = ""; - - if (data.Length <= 0) return outputData; - - if (!data.Contains(line)) - { - return outputData; - } - - try - { - //Debug.LogMessage(LogEventLevel.Verbose, "ParseConsoleData Data: {0}, Line {1}, startStirng {2}, endString {3}", data, line, startString, endString); - var linePosition = data.IndexOf(line, StringComparison.Ordinal); - var startPosition = data.IndexOf(startString, linePosition, StringComparison.Ordinal) + - startString.Length; - var endPosition = data.IndexOf(endString, startPosition, StringComparison.Ordinal); - outputData = data.Substring(startPosition, endPosition - startPosition).Trim(); - //Debug.LogMessage(LogEventLevel.Verbose, "ParseConsoleData Return: {0}", outputData); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Error, "Error Parsing Console Data: {0}", e); - Debug.LogMessage(LogEventLevel.Verbose, "Stack Trace: {stackTrace}", e.StackTrace); - } - - return outputData; - } + /// + /// Gets the CompilerRevisionInfo + /// + [JsonIgnore] + public Version CompilerRevisionInfo + { + get + { + return new Version(CompilerRevision); } } /// - /// Represents a ProgramInfo + /// Gets or sets the CompileTime /// - public class ProgramInfo - { - // Shared properties - - /// - /// Gets the ProgramNumber - /// - [JsonProperty("programNumber")] - public uint ProgramNumber { get; private set; } - - /// - /// Gets or sets the OperatingState - /// - [JsonConverter(typeof (StringEnumConverter))] - [JsonProperty("operatingState")] - public eProgramOperatingState OperatingState { get; set; } - - /// - /// Gets or sets the RegistrationState - /// - [JsonConverter(typeof (StringEnumConverter))] - [JsonProperty("registrationState")] - public eProgramRegistrationState RegistrationState { get; set; } - - /// - /// Gets or sets the ProgramFile - /// - [JsonProperty("programFile")] - public string ProgramFile { get; set; } - - /// - /// Gets or sets the FriendlyName - /// - [JsonProperty("friendlyName")] - public string FriendlyName { get; set; } - - /// - /// Gets or sets the CompilerRevision - /// - [JsonProperty("compilerRevision")] - public string CompilerRevision { get; set; } - - /// - /// Gets the CompilerRevisionInfo - /// - [JsonIgnore] - public Version CompilerRevisionInfo - { - get - { - return new Version(CompilerRevision); - } - } - - /// - /// Gets or sets the CompileTime - /// - [JsonProperty("compileTime")] - public string CompileTime { get; set; } - - /// - /// Gets or sets the Include4Dat - /// - [JsonProperty("include4Dat")] - public string Include4Dat { get; set; } - - // SIMPL Windows properties - - /// - /// Gets or sets the SystemName - /// - [JsonProperty("systemName")] - public string SystemName { get; set; } - - /// - /// Gets or sets the CrestronDb - /// - [JsonProperty("crestronDb")] - public string CrestronDb { get; set; } - - /// - /// Gets or sets the Environment - /// - [JsonProperty("environment")] - public string Environment { get; set; } - - /// - /// Gets or sets the Programmer - /// - [JsonProperty("programmer")] - public string Programmer { get; set; } - - - // SSP Properties - - /// - /// Gets or sets the ApplicationName - /// - [JsonProperty("applicationName")] - public string ApplicationName { get; set; } - - /// - /// Gets or sets the ProgramTool - /// - [JsonProperty("programTool")] - public string ProgramTool { get; set; } - - /// - /// Gets or sets the MinFirmwareVersion - /// - [JsonProperty("minFirmwareVersion")] - public string MinFirmwareVersion { get; set; } - - /// - /// Gets or sets the PlugInVersion - /// - [JsonProperty("plugInVersion")] - public string PlugInVersion { get; set; } - - /// - /// Constructor - /// - /// program slot to get info about - public ProgramInfo(uint number) - { - ProgramNumber = number; - - ProgramFile = ""; - FriendlyName = ""; - CompilerRevision = ""; - CompileTime = ""; - Include4Dat = ""; - - SystemName = ""; - CrestronDb = ""; - Environment = ""; - Programmer = ""; - - ApplicationName = ""; - ProgramTool = ""; - MinFirmwareVersion = ""; - PlugInVersion = ""; - } - } + [JsonProperty("compileTime")] + public string CompileTime { get; set; } /// - /// Represents a ProgramInfoEventArgs + /// Gets or sets the Include4Dat /// - public class ProgramInfoEventArgs : EventArgs - { - /// - /// Gets or sets the ProgramInfo - /// - public ProgramInfo ProgramInfo; + [JsonProperty("include4Dat")] + public string Include4Dat { get; set; } - /// - /// Constructor - /// - /// program info - public ProgramInfoEventArgs(ProgramInfo progInfo) - { - ProgramInfo = progInfo; - } + // SIMPL Windows properties + + /// + /// Gets or sets the SystemName + /// + [JsonProperty("systemName")] + public string SystemName { get; set; } + + /// + /// Gets or sets the CrestronDb + /// + [JsonProperty("crestronDb")] + public string CrestronDb { get; set; } + + /// + /// Gets or sets the Environment + /// + [JsonProperty("environment")] + public string Environment { get; set; } + + /// + /// Gets or sets the Programmer + /// + [JsonProperty("programmer")] + public string Programmer { get; set; } + + + // SSP Properties + + /// + /// Gets or sets the ApplicationName + /// + [JsonProperty("applicationName")] + public string ApplicationName { get; set; } + + /// + /// Gets or sets the ProgramTool + /// + [JsonProperty("programTool")] + public string ProgramTool { get; set; } + + /// + /// Gets or sets the MinFirmwareVersion + /// + [JsonProperty("minFirmwareVersion")] + public string MinFirmwareVersion { get; set; } + + /// + /// Gets or sets the PlugInVersion + /// + [JsonProperty("plugInVersion")] + public string PlugInVersion { get; set; } + + /// + /// Constructor + /// + /// program slot to get info about + public ProgramInfo(uint number) + { + ProgramNumber = number; + + ProgramFile = ""; + FriendlyName = ""; + CompilerRevision = ""; + CompileTime = ""; + Include4Dat = ""; + + SystemName = ""; + CrestronDb = ""; + Environment = ""; + Programmer = ""; + + ApplicationName = ""; + ProgramTool = ""; + MinFirmwareVersion = ""; + PlugInVersion = ""; } -} \ No newline at end of file +} + +/// +/// Represents a ProgramInfoEventArgs +/// +public class ProgramInfoEventArgs : EventArgs +{ + /// + /// Gets or sets the ProgramInfo + /// + public ProgramInfo ProgramInfo; + + /// + /// Constructor + /// + /// program info + public ProgramInfoEventArgs(ProgramInfo progInfo) + { + ProgramInfo = progInfo; + } +} diff --git a/src/PepperDash.Essentials.Core/PartitionSensor/EssentialsPartitionController.cs b/src/PepperDash.Essentials.Core/PartitionSensor/EssentialsPartitionController.cs index ee20a22a..352d7cef 100644 --- a/src/PepperDash.Essentials.Core/PartitionSensor/EssentialsPartitionController.cs +++ b/src/PepperDash.Essentials.Core/PartitionSensor/EssentialsPartitionController.cs @@ -1,228 +1,186 @@ using PepperDash.Core; using System.Collections.Generic; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents an abstract controller device for a partition dividing rooms that are combinable +/// +/// In Auto mode, it can use a partition sensor to automatically determine whether the partition is present. +/// +/// In Manual mode it accepts user input to tell it whether the partition is present. +/// +public class EssentialsPartitionController : IPartitionController { - /// - /// Represents an abstract controller device for a partition dividing rooms that are combinable - /// - /// In Auto mode, it can use a partition sensor to automatically determine whether the partition is present. - /// - /// In Manual mode it accepts user input to tell it whether the partition is present. - /// - public class EssentialsPartitionController : IPartitionController + private IPartitionStateProvider _partitionSensor; + + public bool IsInAutoMode { get; private set; } + + private bool _partitionPresent; + + public bool PartitionPresent { - private IPartitionStateProvider _partitionSensor; - - /// - /// Indicates whether the controller is in Auto mode or Manual mode - /// - public bool IsInAutoMode { get; private set; } - - private bool _partitionPresent; - - /// - /// Gets or sets the PartitionPresent state - /// - public bool PartitionPresent + get { - get + if (IsInAutoMode) { - if (IsInAutoMode) - { - return _partitionSensor.PartitionPresentFeedback.BoolValue; - } - - return _partitionPresent; + return _partitionSensor.PartitionPresentFeedback.BoolValue; } - set + + return _partitionPresent; + } + set + { + if (_partitionPresent == value) { - if (_partitionPresent == value) - { - return; - } + return; + } - _partitionPresent = value; + _partitionPresent = value; - if (PartitionPresentFeedback != null) - { - PartitionPresentFeedback.FireUpdate(); - } + if (PartitionPresentFeedback != null) + { + PartitionPresentFeedback.FireUpdate(); } } + } - /// - /// Constructor - /// - /// key for the partition controller - /// name of the partition controller - /// partition state provider sensor - /// whether to default to manual mode - /// list of adjacent room keys - public EssentialsPartitionController(string key, string name, IPartitionStateProvider sensor, bool defaultToManualMode, List adjacentRoomKeys) + public EssentialsPartitionController(string key, string name, IPartitionStateProvider sensor, bool defaultToManualMode, List adjacentRoomKeys) + { + Key = key; + + Name = name; + + AdjacentRoomKeys = adjacentRoomKeys; + + if (sensor != null) { - Key = key; + _partitionSensor = sensor; - Name = name; - - AdjacentRoomKeys = adjacentRoomKeys; - - if (sensor != null) + if (!defaultToManualMode) { - _partitionSensor = sensor; - - if (!defaultToManualMode) - { - SetAutoMode(); - } - else - { - SetManualMode(); - } + SetAutoMode(); } else { SetManualMode(); } - - PartitionPresentFeedback.FireUpdate(); } - - private void PartitionPresentFeedback_OutputChange(object sender, FeedbackEventArgs e) + else { - if (IsInAutoMode) - { - PartitionPresent = e.BoolValue; - } + SetManualMode(); } - #region IPartitionController Members - - /// - /// Gets or sets the AdjacentRoomKeys - /// - public List AdjacentRoomKeys { get; private set; } - - /// - /// SetAutoMode method - /// - public void SetAutoMode() - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Setting {Key} to Auto Mode", this); - - IsInAutoMode = true; - if (PartitionPresentFeedback != null) - { - PartitionPresentFeedback.SetValueFunc(() => _partitionSensor.PartitionPresentFeedback.BoolValue); - } - else - { - PartitionPresentFeedback = new BoolFeedback(() => _partitionSensor.PartitionPresentFeedback.BoolValue); - } - - if (_partitionSensor != null) - { - _partitionSensor.PartitionPresentFeedback.OutputChange -= PartitionPresentFeedback_OutputChange; - _partitionSensor.PartitionPresentFeedback.OutputChange += PartitionPresentFeedback_OutputChange; - PartitionPresent = _partitionSensor.PartitionPresentFeedback.BoolValue; - } - - PartitionPresentFeedback.FireUpdate(); - } - - /// - /// SetManualMode method - /// - public void SetManualMode() - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Setting {Key} to Manual Mode", this); - - IsInAutoMode = false; - if (PartitionPresentFeedback != null) - { - PartitionPresentFeedback.SetValueFunc(() => _partitionPresent); - } - else - { - PartitionPresentFeedback = new BoolFeedback(() => _partitionPresent); - } - - if (_partitionSensor != null) - { - _partitionSensor.PartitionPresentFeedback.OutputChange -= PartitionPresentFeedback_OutputChange; - PartitionPresent = _partitionSensor.PartitionPresentFeedback.BoolValue; - } - - PartitionPresentFeedback.FireUpdate(); - } - - - /// - /// SetPartitionStatePresent method - /// - public void SetPartitionStatePresent() - { - if (!IsInAutoMode) - { - PartitionPresent = true; - PartitionPresentFeedback.FireUpdate(); - } - } - - /// - /// SetPartitionStateNotPresent method - /// - public void SetPartitionStateNotPresent() - { - if (!IsInAutoMode) - { - PartitionPresent = false; - PartitionPresentFeedback.FireUpdate(); - } - } - - /// - /// ToggglePartitionState method - /// - public void ToggglePartitionState() - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Toggling Partition State for {Key}", this); - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"IsInAutoMode: {IsInAutoMode}", this); - - if (!IsInAutoMode) - { - PartitionPresent = !PartitionPresent; - PartitionPresentFeedback.FireUpdate(); - } - } - - #endregion - - #region IPartitionStateProvider Members - - /// - /// Gets or sets the PartitionPresentFeedback - /// - public BoolFeedback PartitionPresentFeedback { get; private set; } - - #endregion - - #region IKeyName Members - - /// - /// Gets or sets the Name - /// - public string Name { get; private set; } - - #endregion - - #region IKeyed Members - - /// - /// Gets or sets the Key - /// - public string Key { get; private set; } - - #endregion + PartitionPresentFeedback.FireUpdate(); } + + private void PartitionPresentFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + if (IsInAutoMode) + { + PartitionPresent = e.BoolValue; + } + } + + #region IPartitionController Members + + public List AdjacentRoomKeys { get; private set; } + + public void SetAutoMode() + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Setting {Key} to Auto Mode", this); + + IsInAutoMode = true; + if (PartitionPresentFeedback != null) + { + PartitionPresentFeedback.SetValueFunc(() => _partitionSensor.PartitionPresentFeedback.BoolValue); + } + else + { + PartitionPresentFeedback = new BoolFeedback(() => _partitionSensor.PartitionPresentFeedback.BoolValue); + } + + if (_partitionSensor != null) + { + _partitionSensor.PartitionPresentFeedback.OutputChange -= PartitionPresentFeedback_OutputChange; + _partitionSensor.PartitionPresentFeedback.OutputChange += PartitionPresentFeedback_OutputChange; + PartitionPresent = _partitionSensor.PartitionPresentFeedback.BoolValue; + } + + PartitionPresentFeedback.FireUpdate(); + } + + public void SetManualMode() + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Setting {Key} to Manual Mode", this); + + IsInAutoMode = false; + if (PartitionPresentFeedback != null) + { + PartitionPresentFeedback.SetValueFunc(() => _partitionPresent); + } + else + { + PartitionPresentFeedback = new BoolFeedback(() => _partitionPresent); + } + + if (_partitionSensor != null) + { + _partitionSensor.PartitionPresentFeedback.OutputChange -= PartitionPresentFeedback_OutputChange; + PartitionPresent = _partitionSensor.PartitionPresentFeedback.BoolValue; + } + + PartitionPresentFeedback.FireUpdate(); + } + + + public void SetPartitionStatePresent() + { + if (!IsInAutoMode) + { + PartitionPresent = true; + PartitionPresentFeedback.FireUpdate(); + } + } + + public void SetPartitionStateNotPresent() + { + if (!IsInAutoMode) + { + PartitionPresent = false; + PartitionPresentFeedback.FireUpdate(); + } + } + + public void ToggglePartitionState() + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Toggling Partition State for {Key}", this); + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"IsInAutoMode: {IsInAutoMode}", this); + + if (!IsInAutoMode) + { + PartitionPresent = !PartitionPresent; + PartitionPresentFeedback.FireUpdate(); + } + } + + #endregion + + #region IPartitionStateProvider Members + + public BoolFeedback PartitionPresentFeedback { get; private set; } + + #endregion + + #region IKeyName Members + + public string Name { get; private set; } + + #endregion + + #region IKeyed Members + + public string Key { get; private set; } + + #endregion } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/PartitionSensor/IPartitionStateProvider.cs b/src/PepperDash.Essentials.Core/PartitionSensor/IPartitionStateProvider.cs index 5bf40345..89f2a114 100644 --- a/src/PepperDash.Essentials.Core/PartitionSensor/IPartitionStateProvider.cs +++ b/src/PepperDash.Essentials.Core/PartitionSensor/IPartitionStateProvider.cs @@ -2,66 +2,66 @@ using Newtonsoft.Json; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Defines the contract for IPartitionStateProvider +/// +public interface IPartitionStateProvider : IKeyName { /// - /// Defines the contract for IPartitionStateProvider + /// Feedback indicating whether the partition is present /// - public interface IPartitionStateProvider : IKeyName - { - /// - /// Feedback indicating whether the partition is present - /// - [JsonIgnore] - BoolFeedback PartitionPresentFeedback { get; } - - /// - /// Indicates whether the partition is present - /// - [JsonProperty("partitionPresent")] - bool PartitionPresent { get; } - } + [JsonIgnore] + BoolFeedback PartitionPresentFeedback { get; } /// - /// Defines the contract for IPartitionController + /// Indicates whether the partition is present /// - public interface IPartitionController : IPartitionStateProvider - { - /// - /// List of adjacent room keys - /// - [JsonProperty("adjacentRoomKeys")] - List AdjacentRoomKeys { get; } + [JsonProperty("partitionPresent")] + bool PartitionPresent { get; } +} - /// - /// Indicates whether the controller is in Auto mode or Manual mode - /// - [JsonProperty("isInAutoMode")] - bool IsInAutoMode { get; } +/// +/// Defines the contract for IPartitionController +/// +public interface IPartitionController : IPartitionStateProvider +{ + /// + /// List of adjacent room keys + /// + [JsonProperty("adjacentRoomKeys")] + List AdjacentRoomKeys { get; } - /// - /// Sets the PartitionPresent state - /// - void SetPartitionStatePresent(); + /// + /// Indicates whether the controller is in Auto mode or Manual mode + /// + [JsonProperty("isInAutoMode")] + bool IsInAutoMode { get; } - /// - /// Sets the PartitionPresent state to not present - /// - void SetPartitionStateNotPresent(); + /// + /// Sets the PartitionPresent state + /// + void SetPartitionStatePresent(); - /// - /// Toggles the PartitionPresent state - /// - void ToggglePartitionState(); + /// + /// Sets the PartitionPresent state to not present + /// + void SetPartitionStateNotPresent(); - /// - /// Sets the controller to Manual mode - /// - void SetManualMode(); + /// + /// Toggles the PartitionPresent state + /// + void ToggglePartitionState(); - /// - /// Sets the controller to Auto mode - /// - void SetAutoMode(); - } -} \ No newline at end of file + /// + /// Sets the controller to Manual mode + /// + void SetManualMode(); + + /// + /// Sets the controller to Auto mode + /// + void SetAutoMode(); +} diff --git a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs index caee2dcd..116c4aa1 100644 --- a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs @@ -2,18 +2,28 @@ using PepperDash.Essentials.Core.Config; using System.Collections.Generic; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + +/// +/// Defines a class that is capable of loading custom plugin device types +/// +public interface IPluginDeviceFactory : IDeviceFactory +{ /// - /// Defines a class that is capable of loading custom plugin device types + /// Required to define the minimum version for Essentials in the format xx.yy.zz /// - public interface IPluginDeviceFactory : IDeviceFactory - { - /// - /// Required to define the minimum version for Essentials in the format xx.yy.zz - /// - string MinimumEssentialsFrameworkVersion { get; } - } + string MinimumEssentialsFrameworkVersion { get; } } - +/// +/// Defines a factory for creating plugin development devices, including support for specific framework versions. +/// +/// This interface extends to provide additional functionality +/// specific to plugin development, such as access to supported framework versions. +public interface IPluginDevelopmentDeviceFactory : IPluginDeviceFactory +{ + /// + /// Gets a list of framework versions that are essential for development. + /// + List DevelopmentEssentialsFrameworkVersions { get; } +} diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index 49ebad46..deec08cb 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -734,21 +734,22 @@ public static class PluginLoader { try { - if (typeof(IPluginDeviceFactory).IsAssignableFrom(type) && !type.IsAbstract) + if (!typeof(IPluginDeviceFactory).IsAssignableFrom(type) || type.IsAbstract) { - var plugin = - (IPluginDeviceFactory)Activator.CreateInstance(type); - LoadCustomPlugin(plugin, loadedAssembly); + continue; } + + var plugin = (IPluginDeviceFactory)Activator.CreateInstance(type); + LoadCustomPlugin(plugin, loadedAssembly); } catch (NotSupportedException) { //this happens for dlls that aren't PD dlls, like ports of Mono classes into S#. Swallowing. } - catch (Exception e) + catch (Exception ex) { - Debug.LogMessage(LogEventLevel.Error, "Load Plugin not found. {0}.{2} is not a plugin factory. Exception: {1}", - loadedAssembly.Name, e.Message, type.Name); + Debug.LogError("Load Plugin not found. {assemblyName}.{typeName} is not a plugin factory. Exception: {exception}", + loadedAssembly.Name, type.Name, ex.Message); continue; } } @@ -830,7 +831,11 @@ public static class PluginLoader /// The assembly associated with the plugin being loaded. This is used for logging and tracking purposes. private static void LoadCustomPlugin(IPluginDeviceFactory deviceFactory, LoadedAssembly loadedAssembly) { - var passed = Global.IsRunningMinimumVersionOrHigher(deviceFactory.MinimumEssentialsFrameworkVersion); + var developmentDeviceFactory = deviceFactory as IPluginDevelopmentDeviceFactory; + + var passed = developmentDeviceFactory != null ? Global.IsRunningDevelopmentVersion + (developmentDeviceFactory.DevelopmentEssentialsFrameworkVersions, developmentDeviceFactory.MinimumEssentialsFrameworkVersion) + : Global.IsRunningMinimumVersionOrHigher(deviceFactory.MinimumEssentialsFrameworkVersion); if (!passed) { diff --git a/src/PepperDash.Essentials.Core/Presets/DevicePresets.cs b/src/PepperDash.Essentials.Core/Presets/DevicePresets.cs index 253a19e1..7346981b 100644 --- a/src/PepperDash.Essentials.Core/Presets/DevicePresets.cs +++ b/src/PepperDash.Essentials.Core/Presets/DevicePresets.cs @@ -12,284 +12,40 @@ using PepperDash.Core; using PepperDash.Core.WebApi.Presets; using Serilog.Events; -namespace PepperDash.Essentials.Core.Presets +namespace PepperDash.Essentials.Core.Presets; + +/// +/// Class that represents the model behind presets display +/// +public class DevicePresetsModel : Device { + public delegate void PresetRecalledCallback(ISetTopBoxNumericKeypad device, string channel); + + public delegate void PresetsSavedCallback(List presets); + + private readonly CCriticalSection _fileOps = new CCriticalSection(); + private readonly bool _initSuccess; + + private readonly ISetTopBoxNumericKeypad _setTopBox; + /// - /// Class that represents the model behind presets display + /// The methods on the STB device to call when dialing /// - public class DevicePresetsModel : Device + private Dictionary> _dialFunctions; + + private bool _dialIsRunning; + private Action _enterFunction; + private string _filePath; + + public DevicePresetsModel(string key, ISetTopBoxNumericKeypad setTopBox, string fileName) + : this(key, fileName) { - /// - /// Delegate for preset recalled event - /// - /// device that recalled a preset - /// channel that was recalled - public delegate void PresetRecalledCallback(ISetTopBoxNumericKeypad device, string channel); - - /// - /// Delegate for presets saved event - /// - /// list of presets that were saved - public delegate void PresetsSavedCallback(List presets); - - private readonly CCriticalSection _fileOps = new CCriticalSection(); - private readonly bool _initSuccess; - - private readonly ISetTopBoxNumericKeypad _setTopBox; - - /// - /// The methods on the STB device to call when dialing - /// - private Dictionary> _dialFunctions; - - private bool _dialIsRunning; - private Action _enterFunction; - private string _filePath; - - /// - /// Constructor - /// - /// key for the device - /// set top box device - /// file name for presets - public DevicePresetsModel(string key, ISetTopBoxNumericKeypad setTopBox, string fileName) - : this(key, fileName) + try { - try - { - _setTopBox = setTopBox; + _setTopBox = setTopBox; - // Grab the digit functions from the device - // If any fail, the whole thing fails peacefully - _dialFunctions = new Dictionary>(10) - { - {'1', setTopBox.Digit1}, - {'2', setTopBox.Digit2}, - {'3', setTopBox.Digit3}, - {'4', setTopBox.Digit4}, - {'5', setTopBox.Digit5}, - {'6', setTopBox.Digit6}, - {'7', setTopBox.Digit7}, - {'8', setTopBox.Digit8}, - {'9', setTopBox.Digit9}, - {'0', setTopBox.Digit0}, - {'-', setTopBox.Dash} - }; - } - catch - { - Debug.LogMessage(LogEventLevel.Information, "DevicePresets '{0}', not attached to INumericKeypad device. Ignoring", key); - _dialFunctions = null; - return; - } - - _enterFunction = setTopBox.KeypadEnter; - } - - /// - /// Constructor - /// - /// key for the device - /// file name for presets - public DevicePresetsModel(string key, string fileName) : base(key) - { - PulseTime = 150; - DigitSpacingMs = 150; - - UseLocalImageStorage = true; - - ImagesLocalHostPrefix = "http://" + CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); - ImagesPathPrefix = @"/presets/images.zip/"; - ListPathPrefix = @"/html/presets/lists/"; - - SetFileName(fileName); - - _initSuccess = true; - } - - /// - /// Event fired when a preset is recalled - /// - public event PresetRecalledCallback PresetRecalled; - - /// - /// Event fired when presets are saved - /// - public event PresetsSavedCallback PresetsSaved; - - /// - /// Gets or sets the PulseTime - /// - public int PulseTime { get; set; } - - /// - /// Gets or sets the DigitSpacingMs - /// - public int DigitSpacingMs { get; set; } - - /// - /// Gets or sets the PresetsAreLoaded - /// - public bool PresetsAreLoaded { get; private set; } - - /// - /// Gets or sets the PresetsList - /// - public List PresetsList { get; private set; } - - /// - /// Gets the Count of presets - /// - public int Count - { - get { return PresetsList != null ? PresetsList.Count : 0; } - } - - /// - /// Gets or sets the UseLocalImageStorage - /// - public bool UseLocalImageStorage { get; set; } - - /// - /// Gets or sets the ImagesLocalHostPrefix - /// - public string ImagesLocalHostPrefix { get; set; } - - /// - /// Gets or sets the ImagesPathPrefix - /// - public string ImagesPathPrefix { get; set; } - - /// - /// Gets or sets the ListPathPrefix - /// - public string ListPathPrefix { get; set; } - - /// - /// Event fired when presets are loaded - /// - public event EventHandler PresetsLoaded; - - - /// - /// SetFileName method - /// - public void SetFileName(string path) - { - _filePath = ListPathPrefix + path; - - Debug.LogMessage(LogEventLevel.Verbose, this, "Setting presets file path to {0}", _filePath); - LoadChannels(); - } - - /// - /// LoadChannels method - /// - public void LoadChannels() - { - try - { - _fileOps.Enter(); - - Debug.LogMessage(LogEventLevel.Verbose, this, "Loading presets from {0}", _filePath); - PresetsAreLoaded = false; - try - { - var pl = JsonConvert.DeserializeObject(File.ReadToEnd(_filePath, Encoding.ASCII)); - Name = pl.Name; - PresetsList = pl.Channels; - - Debug.LogMessage(LogEventLevel.Verbose, this, "Loaded {0} presets", PresetsList.Count); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Verbose, this, - "LoadChannels: Error reading presets file. These presets will be empty:\r '{0}'\r Error:{1}", - _filePath, e.Message); - // Just save a default empty list - PresetsList = new List(); - } - PresetsAreLoaded = true; - - var handler = PresetsLoaded; - if (handler != null) - { - handler(this, EventArgs.Empty); - } - } - finally - { - _fileOps.Leave(); - } - } - - /// - /// Dial method - /// - public void Dial(int presetNum) - { - if (presetNum <= PresetsList.Count) - { - Dial(PresetsList[presetNum - 1].Channel); - } - } - - /// - /// Dial method - /// - public void Dial(string chanNum) - { - if (_dialIsRunning || !_initSuccess) - { - return; - } - if (_dialFunctions == null) - { - Debug.LogMessage(LogEventLevel.Debug, "DevicePresets '{0}', not attached to keypad device. Ignoring channel", Key); - return; - } - - _dialIsRunning = true; - CrestronInvoke.BeginInvoke(o => - { - foreach (var c in chanNum.ToCharArray()) - { - if (_dialFunctions.ContainsKey(c)) - { - Pulse(_dialFunctions[c]); - } - CrestronEnvironment.Sleep(DigitSpacingMs); - } - - if (_enterFunction != null) - { - Pulse(_enterFunction); - } - _dialIsRunning = false; - }); - - if (_setTopBox == null) return; - - OnPresetRecalled(_setTopBox, chanNum); - } - - /// - /// Dial method - /// - public void Dial(int presetNum, ISetTopBoxNumericKeypad setTopBox) - { - if (presetNum <= PresetsList.Count) - { - Dial(PresetsList[presetNum - 1].Channel, setTopBox); - } - } - - /// - /// Dial method - /// - public void Dial(string chanNum, ISetTopBoxNumericKeypad setTopBox) - { + // Grab the digit functions from the device + // If any fail, the whole thing fails peacefully _dialFunctions = new Dictionary>(10) { {'1', setTopBox.Digit1}, @@ -304,89 +60,245 @@ namespace PepperDash.Essentials.Core.Presets {'0', setTopBox.Digit0}, {'-', setTopBox.Dash} }; - - _enterFunction = setTopBox.KeypadEnter; - - OnPresetRecalled(setTopBox, chanNum); - - Dial(chanNum); + } + catch + { + Debug.LogMessage(LogEventLevel.Information, "DevicePresets '{0}', not attached to INumericKeypad device. Ignoring", key); + _dialFunctions = null; + return; } - private void OnPresetRecalled(ISetTopBoxNumericKeypad setTopBox, string channel) + _enterFunction = setTopBox.KeypadEnter; + } + + public DevicePresetsModel(string key, string fileName) : base(key) + { + PulseTime = 150; + DigitSpacingMs = 150; + + UseLocalImageStorage = true; + + ImagesLocalHostPrefix = "http://" + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + ImagesPathPrefix = @"/presets/images.zip/"; + ListPathPrefix = @"/html/presets/lists/"; + + SetFileName(fileName); + + _initSuccess = true; + } + + public event PresetRecalledCallback PresetRecalled; + public event PresetsSavedCallback PresetsSaved; + + public int PulseTime { get; set; } + public int DigitSpacingMs { get; set; } + public bool PresetsAreLoaded { get; private set; } + + public List PresetsList { get; private set; } + + public int Count + { + get { return PresetsList != null ? PresetsList.Count : 0; } + } + + public bool UseLocalImageStorage { get; set; } + public string ImagesLocalHostPrefix { get; set; } + public string ImagesPathPrefix { get; set; } + public string ListPathPrefix { get; set; } + public event EventHandler PresetsLoaded; + + + public void SetFileName(string path) + { + _filePath = ListPathPrefix + path; + + Debug.LogMessage(LogEventLevel.Verbose, this, "Setting presets file path to {0}", _filePath); + LoadChannels(); + } + + public void LoadChannels() + { + try { - var handler = PresetRecalled; + _fileOps.Enter(); - if (handler == null) - { - return; - } - - handler(setTopBox, channel); - } - - /// - /// UpdatePreset method - /// - public void UpdatePreset(int index, PresetChannel preset) - { - if (index >= PresetsList.Count) - { - return; - } - - PresetsList[index] = preset; - - SavePresets(); - - OnPresetsSaved(); - } - - /// - /// UpdatePresets method - /// - public void UpdatePresets(List presets) - { - PresetsList = presets; - - SavePresets(); - - OnPresetsSaved(); - } - - private void SavePresets() - { + Debug.LogMessage(LogEventLevel.Verbose, this, "Loading presets from {0}", _filePath); + PresetsAreLoaded = false; try { - _fileOps.Enter(); - var pl = new PresetsList {Channels = PresetsList, Name = Name}; - var json = JsonConvert.SerializeObject(pl, Formatting.Indented); + var pl = JsonConvert.DeserializeObject(File.ReadToEnd(_filePath, Encoding.ASCII)); + Name = pl.Name; + PresetsList = pl.Channels; - using (var file = File.Open(_filePath, FileMode.Truncate)) - { - file.Write(json, Encoding.UTF8); - } + Debug.LogMessage(LogEventLevel.Verbose, this, "Loaded {0} presets", PresetsList.Count); } - finally + catch (Exception e) { - _fileOps.Leave(); + Debug.LogMessage(LogEventLevel.Verbose, this, + "LoadChannels: Error reading presets file. These presets will be empty:\r '{0}'\r Error:{1}", + _filePath, e.Message); + // Just save a default empty list + PresetsList = new List(); + } + PresetsAreLoaded = true; + + var handler = PresetsLoaded; + if (handler != null) + { + handler(this, EventArgs.Empty); } - } - - private void OnPresetsSaved() + finally { - var handler = PresetsSaved; - - if (handler == null) return; - - handler(PresetsList); - } - - private void Pulse(Action act) - { - act(true); - CrestronEnvironment.Sleep(PulseTime); - act(false); + _fileOps.Leave(); } } + + public void Dial(int presetNum) + { + if (presetNum <= PresetsList.Count) + { + Dial(PresetsList[presetNum - 1].Channel); + } + } + + public void Dial(string chanNum) + { + if (_dialIsRunning || !_initSuccess) + { + return; + } + if (_dialFunctions == null) + { + Debug.LogMessage(LogEventLevel.Debug, "DevicePresets '{0}', not attached to keypad device. Ignoring channel", Key); + return; + } + + _dialIsRunning = true; + CrestronInvoke.BeginInvoke(o => + { + foreach (var c in chanNum.ToCharArray()) + { + if (_dialFunctions.ContainsKey(c)) + { + Pulse(_dialFunctions[c]); + } + CrestronEnvironment.Sleep(DigitSpacingMs); + } + + if (_enterFunction != null) + { + Pulse(_enterFunction); + } + _dialIsRunning = false; + }); + + if (_setTopBox == null) return; + + OnPresetRecalled(_setTopBox, chanNum); + } + + public void Dial(int presetNum, ISetTopBoxNumericKeypad setTopBox) + { + if (presetNum <= PresetsList.Count) + { + Dial(PresetsList[presetNum - 1].Channel, setTopBox); + } + } + + public void Dial(string chanNum, ISetTopBoxNumericKeypad setTopBox) + { + _dialFunctions = new Dictionary>(10) + { + {'1', setTopBox.Digit1}, + {'2', setTopBox.Digit2}, + {'3', setTopBox.Digit3}, + {'4', setTopBox.Digit4}, + {'5', setTopBox.Digit5}, + {'6', setTopBox.Digit6}, + {'7', setTopBox.Digit7}, + {'8', setTopBox.Digit8}, + {'9', setTopBox.Digit9}, + {'0', setTopBox.Digit0}, + {'-', setTopBox.Dash} + }; + + _enterFunction = setTopBox.KeypadEnter; + + OnPresetRecalled(setTopBox, chanNum); + + Dial(chanNum); + } + + private void OnPresetRecalled(ISetTopBoxNumericKeypad setTopBox, string channel) + { + var handler = PresetRecalled; + + if (handler == null) + { + return; + } + + handler(setTopBox, channel); + } + + public void UpdatePreset(int index, PresetChannel preset) + { + if (index >= PresetsList.Count) + { + return; + } + + PresetsList[index] = preset; + + SavePresets(); + + OnPresetsSaved(); + } + + public void UpdatePresets(List presets) + { + PresetsList = presets; + + SavePresets(); + + OnPresetsSaved(); + } + + private void SavePresets() + { + try + { + _fileOps.Enter(); + var pl = new PresetsList {Channels = PresetsList, Name = Name}; + var json = JsonConvert.SerializeObject(pl, Formatting.Indented); + + using (var file = File.Open(_filePath, FileMode.Truncate)) + { + file.Write(json, Encoding.UTF8); + } + } + finally + { + _fileOps.Leave(); + } + + } + + private void OnPresetsSaved() + { + var handler = PresetsSaved; + + if (handler == null) return; + + handler(PresetsList); + } + + private void Pulse(Action act) + { + act(true); + CrestronEnvironment.Sleep(PulseTime); + act(false); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Presets/DevicePresetsView.cs b/src/PepperDash.Essentials.Core/Presets/DevicePresetsView.cs index 42761de7..57be7624 100644 --- a/src/PepperDash.Essentials.Core/Presets/DevicePresetsView.cs +++ b/src/PepperDash.Essentials.Core/Presets/DevicePresetsView.cs @@ -6,88 +6,87 @@ using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core.Presets +namespace PepperDash.Essentials.Core.Presets; + +/// +/// Represents a DevicePresetsView +/// +public class DevicePresetsView { /// - /// Represents a DevicePresetsView + /// Gets or sets the ShowNumbers /// - public class DevicePresetsView + public bool ShowNumbers { get; set; } + + /// + /// Gets or sets the ShowName + /// + public bool ShowName { get; set; } + + /// + /// Gets or sets the ShowIcon + /// + public bool ShowIcon { get; set; } + + /// + /// Gets the SubpageReferenceList + /// + public SubpageReferenceList SRL { get; private set; } + + /// + /// Gets the DevicePresetsModel + /// + public DevicePresetsModel Model { get; private set; } + + /// + /// Constructor + /// + /// trilst + /// device presets model + /// + public DevicePresetsView(BasicTriListWithSmartObject tl, DevicePresetsModel model) { - /// - /// Gets or sets the ShowNumbers - /// - public bool ShowNumbers { get; set; } - - /// - /// Gets or sets the ShowName - /// - public bool ShowName { get; set; } - - /// - /// Gets or sets the ShowIcon - /// - public bool ShowIcon { get; set; } - - /// - /// Gets or sets the SRL - /// - public SubpageReferenceList SRL { get; private set; } - - /// - /// Gets or sets the Model - /// - public DevicePresetsModel Model { get; private set; } - - /// - /// Constructor - /// - /// trilst - /// device presets model - /// - public DevicePresetsView(BasicTriListWithSmartObject tl, DevicePresetsModel model) + if (model == null) { - if (model == null) + throw new ArgumentNullException("model", "DevicePresetsView Cannot be instantiated with null model"); + } + ShowIcon = true; + ShowName = true; + + Model = model; + + SRL = new SubpageReferenceList(tl, 10012, 3, 0, 4); + Model.PresetsLoaded += new EventHandler(Model_PresetsLoaded); + } + + /// + /// Attach method + /// + public void Attach() + { + if (Model.PresetsAreLoaded) + { + uint index = 1; + foreach (var p in Model.PresetsList) { - throw new ArgumentNullException("model", "DevicePresetsView Cannot be instantiated with null model"); + SRL.AddItem(new PresetsListSubpageReferenceListItem(p, index, SRL, this)); + index++; } - ShowIcon = true; - ShowName = true; - - Model = model; - - SRL = new SubpageReferenceList(tl, 10012, 3, 0, 4); - Model.PresetsLoaded += new EventHandler(Model_PresetsLoaded); - } - - /// - /// Attach method - /// - public void Attach() - { - if (Model.PresetsAreLoaded) - { - uint index = 1; - foreach (var p in Model.PresetsList) - { - SRL.AddItem(new PresetsListSubpageReferenceListItem(p, index, SRL, this)); - index++; - } - SRL.Count = (ushort)Model.PresetsList.Count; - } - } - - /// - /// Detach method - /// - public void Detach() - { - SRL.Clear(); - } - - void Model_PresetsLoaded(object sender, EventArgs e) - { - Detach(); - Attach(); + SRL.Count = (ushort)Model.PresetsList.Count; } } + + /// + /// Detach method + /// + public void Detach() + { + SRL.Clear(); + } + + void Model_PresetsLoaded(object sender, EventArgs e) + { + Detach(); + Attach(); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Presets/PresetChannel.cs b/src/PepperDash.Essentials.Core/Presets/PresetChannel.cs index 8fd15105..d7a9ffa9 100644 --- a/src/PepperDash.Essentials.Core/Presets/PresetChannel.cs +++ b/src/PepperDash.Essentials.Core/Presets/PresetChannel.cs @@ -7,48 +7,47 @@ using System.Text; using Crestron.SimplSharp; using Newtonsoft.Json; -namespace PepperDash.Essentials.Core.Presets +namespace PepperDash.Essentials.Core.Presets; + +/// +/// Represents a PresetChannel +/// +public class PresetChannel { - /// - /// Represents a PresetChannel - /// - public class PresetChannel - { - - /// - /// Gets or sets the Name - /// - [JsonProperty(Required = Required.Always,PropertyName = "name")] - public string Name { get; set; } - - /// - /// Gets or sets the IconUrl - /// - [JsonProperty(Required = Required.Always, PropertyName = "iconUrl")] - public string IconUrl { get; set; } - - /// - /// Gets or sets the Channel - /// - [JsonProperty(Required = Required.Always, PropertyName = "channel")] - public string Channel { get; set; } - } /// - /// Represents a PresetsList + /// Gets or sets the Name /// - public class PresetsList - { - /// - /// Gets or sets the Name - /// - [JsonProperty(Required=Required.Always,PropertyName = "name")] - public string Name { get; set; } + [JsonProperty(Required = Required.Always, PropertyName = "name")] + public string Name { get; set; } - /// - /// Gets or sets the Channels - /// - [JsonProperty(Required = Required.Always, PropertyName = "channels")] - public List Channels { get; set; } - } + /// + /// Gets or sets the IconUrl + /// + [JsonProperty(Required = Required.Always, PropertyName = "iconUrl")] + public string IconUrl { get; set; } + + /// + /// Gets or sets the Channel + /// + [JsonProperty(Required = Required.Always, PropertyName = "channel")] + public string Channel { get; set; } +} + +/// +/// Represents a PresetsList +/// +public class PresetsList +{ + /// + /// Gets or sets the Name + /// + [JsonProperty(Required = Required.Always, PropertyName = "name")] + public string Name { get; set; } + + /// + /// Gets or sets the Channels + /// + [JsonProperty(Required = Required.Always, PropertyName = "channels")] + public List Channels { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Presets/PresetsListSubpageReferenceListItem.cs b/src/PepperDash.Essentials.Core/Presets/PresetsListSubpageReferenceListItem.cs index f7bfa3bd..3453c3ad 100644 --- a/src/PepperDash.Essentials.Core/Presets/PresetsListSubpageReferenceListItem.cs +++ b/src/PepperDash.Essentials.Core/Presets/PresetsListSubpageReferenceListItem.cs @@ -10,11 +10,8 @@ using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.Presets -{ - /// - /// Represents a PresetsListSubpageReferenceListItem - /// +namespace PepperDash.Essentials.Core.Presets; + public class PresetsListSubpageReferenceListItem : SubpageReferenceListItem { DevicePresetsView View; @@ -64,5 +61,4 @@ namespace PepperDash.Essentials.Core.Presets var icon = View.ShowIcon ? url : ""; Owner.StringInputSig(Index, 3).StringValue = icon; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Queues/ComsMessage.cs b/src/PepperDash.Essentials.Core/Queues/ComsMessage.cs index 1eb3a281..5d17b7fe 100644 --- a/src/PepperDash.Essentials.Core/Queues/ComsMessage.cs +++ b/src/PepperDash.Essentials.Core/Queues/ComsMessage.cs @@ -1,74 +1,72 @@ using System; using PepperDash.Core; -namespace PepperDash.Essentials.Core.Queues +namespace PepperDash.Essentials.Core.Queues; + +/// +/// IBasicCommunication Message for IQueue +/// +public class ComsMessage : IQueueMessage { + private readonly byte[] _bytes; + private readonly IBasicCommunication _coms; + private readonly string _string; + private readonly bool _isByteMessage; + /// /// Represents a ComsMessage /// - public class ComsMessage : IQueueMessage + /// IBasicCommunication to send the message + /// Message to send + public ComsMessage(IBasicCommunication coms, string message) { - private readonly byte[] _bytes; - private readonly IBasicCommunication _coms; - private readonly string _string; - private readonly bool _isByteMessage; + Validate(coms, message); + _coms = coms; + _string = message; + } - /// - /// Constructor for a string message - /// - /// IBasicCommunication to send the message - /// Message to send - public ComsMessage(IBasicCommunication coms, string message) + /// + /// Constructor for a byte message + /// + /// IBasicCommunication to send the message + /// Message to send + public ComsMessage(IBasicCommunication coms, byte[] message) + { + Validate(coms, message); + _coms = coms; + _bytes = message; + _isByteMessage = true; + } + + private void Validate(IBasicCommunication coms, object message) + { + if (coms == null) + throw new ArgumentNullException("coms"); + + if (message == null) + throw new ArgumentNullException("message"); + } + + /// + /// Dispatchs the string/byte[] to the IBasicCommunication specified + /// + public void Dispatch() + { + if (_isByteMessage) { - Validate(coms, message); - _coms = coms; - _string = message; + _coms.SendBytes(_bytes); } - - /// - /// Constructor for a byte message - /// - /// IBasicCommunication to send the message - /// Message to send - public ComsMessage(IBasicCommunication coms, byte[] message) + else { - Validate(coms, message); - _coms = coms; - _bytes = message; - _isByteMessage = true; - } - - private void Validate(IBasicCommunication coms, object message) - { - if (coms == null) - throw new ArgumentNullException("coms"); - - if (message == null) - throw new ArgumentNullException("message"); - } - - /// - /// Dispatch method - /// - public void Dispatch() - { - if (_isByteMessage) - { - _coms.SendBytes(_bytes); - } - else - { - _coms.SendText(_string); - } - } - - /// - /// ToString method - /// - /// - public override string ToString() - { - return _bytes != null ? _bytes.ToString() : _string; + _coms.SendText(_string); } } + + /// + /// Shows either the byte[] or string to be sent + /// + public override string ToString() + { + return _bytes != null ? _bytes.ToString() : _string; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Queues/GenericQueue.cs b/src/PepperDash.Essentials.Core/Queues/GenericQueue.cs index af76f10f..8cb26524 100644 --- a/src/PepperDash.Essentials.Core/Queues/GenericQueue.cs +++ b/src/PepperDash.Essentials.Core/Queues/GenericQueue.cs @@ -6,282 +6,264 @@ using PepperDash.Core; using Serilog.Events; using Thread = Crestron.SimplSharpPro.CrestronThread.Thread; -namespace PepperDash.Essentials.Core.Queues +namespace PepperDash.Essentials.Core.Queues; + +/// +/// Threadsafe processing of queued items with pacing if required +/// +public class GenericQueue : IQueue { + private readonly string _key; + protected readonly ConcurrentQueue _queue; + protected readonly Thread _worker; + protected readonly CEvent _waitHandle = new CEvent(); + + private bool _delayEnabled; + private int _delayTime; + + private const Thread.eThreadPriority _defaultPriority = Thread.eThreadPriority.MediumPriority; + /// - /// Threadsafe processing of queued items with pacing if required + /// If the instance has been disposed. /// - public class GenericQueue : IQueue + public bool Disposed { get; private set; } + + /// + /// Returns the capacity of the CrestronQueue (fixed Size property) + /// + public int QueueCapacity { - private readonly string _key; - - /// - /// The internal queue - /// - protected readonly ConcurrentQueue _queue; - - /// - /// The worker thread - /// - protected readonly Thread _worker; - - /// - /// The wait handle for the queue - /// - protected readonly CEvent _waitHandle = new CEvent(); - - private bool _delayEnabled; - private int _delayTime; - - private const Thread.eThreadPriority _defaultPriority = Thread.eThreadPriority.MediumPriority; - - /// - /// Gets or sets the Disposed - /// - public bool Disposed { get; private set; } - - /// - /// Returns the capacity of the CrestronQueue (fixed Size property) - /// - public int QueueCapacity + get { - get - { - return int.MaxValue; - } - } - - /// - /// Returns the number of elements currently in the CrestronQueue - /// - public int QueueCount - { - get - { - return _queue.Count; - } - } - - /// - /// Constructor with no thread priority - /// - /// - public GenericQueue(string key) - : this(key, _defaultPriority, 0, 0) - { - } - - /// - /// Constructor with queue size - /// - /// - /// Fixed size for the queue to hold - public GenericQueue(string key, int capacity) - : this(key, _defaultPriority, capacity, 0) - { - } - - /// - /// Constructor for generic queue with no pacing - /// - /// Key - /// Pacing in ms between actions - public GenericQueue(int pacing, string key) - : this(key, _defaultPriority, 0, pacing) - { - } - - /// - /// Constructor with pacing and capacity - /// - /// - /// - /// - public GenericQueue(string key, int pacing, int capacity) - : this(key, _defaultPriority, capacity, pacing) - { - } - - /// - /// Constructor with pacing and priority - /// - /// - /// - /// - public GenericQueue(string key, int pacing, Thread.eThreadPriority priority) - : this(key, priority, 0, pacing) - { - } - - /// - /// Constructor with pacing, priority and capacity - /// - /// - /// - /// - public GenericQueue(string key, Thread.eThreadPriority priority, int capacity) - : this(key, priority, capacity, 0) - { - } - - /// - /// Constructor with pacing, priority and capacity - /// - /// - /// - /// - /// - public GenericQueue(string key, int pacing, Thread.eThreadPriority priority, int capacity) - : this(key, priority, capacity, pacing) - { - } - - /// - /// Constructor for generic queue with no pacing - /// - /// Key - /// - /// - /// - protected GenericQueue(string key, Thread.eThreadPriority priority, int capacity, int pacing) - { - _key = key; - int cap = 25; // sets default - if (capacity > 0) - { - cap = capacity; // overrides default - } - - _queue = new ConcurrentQueue(); - _worker = new Thread(ProcessQueue, null, Thread.eThreadStartOptions.Running) - { - Priority = priority, - Name = _key - }; - - SetDelayValues(pacing); - } - - private void SetDelayValues(int pacing) - { - _delayEnabled = pacing > 0; - _delayTime = pacing; - - CrestronEnvironment.ProgramStatusEventHandler += programEvent => - { - if (programEvent != eProgramStatusEventType.Stopping) - return; - - Dispose(true); - }; - } - - /// - /// Thread callback - /// - /// The action used to process dequeued items - /// Null when the thread is exited - private object ProcessQueue(object obj) - { - while (true) - { - if (_queue.TryDequeue(out var item) && item == null) - break; - - if (item != null) - { - try - { - //Debug.LogMessage(LogEventLevel.Verbose, this, "Processing queue item: '{0}'", item.ToString()); - item.Dispatch(); - - if (_delayEnabled) - Thread.Sleep(_delayTime); - } - catch (ThreadAbortException) - { - //swallowing this exception, as it should only happen on shut down - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Information, this, "Caught an exception in the Queue: {1}:{0}", ex.Message, ex); - Debug.LogMessage(LogEventLevel.Verbose, this, "Stack Trace: {0}", ex.StackTrace); - - if (ex.InnerException != null) - { - Debug.LogMessage(LogEventLevel.Information, this, "---\r\n{0}", ex.InnerException.Message); - Debug.LogMessage(LogEventLevel.Verbose, this, "Stack Trace: {0}", ex.InnerException.StackTrace); - } - } - } - else _waitHandle.Wait(); - } - - return null; - } - - /// - /// Enqueue method - /// - public void Enqueue(IQueueMessage item) - { - if (Disposed) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Queue has been disposed. Enqueuing messages not allowed while program is stopping."); - return; - } - - _queue.Enqueue(item); - _waitHandle.Set(); - } - - /// - /// Dispose method - /// - public void Dispose() - { - Dispose(true); - CrestronEnvironment.GC.SuppressFinalize(this); - } - - /// - /// Actually does the disposing. If you override this method, be sure to either call the base implementation - /// or clean up all the resources yourself. - /// - /// set to true unless called from finalizer - protected void Dispose(bool disposing) - { - if (Disposed) - return; - - if (disposing) - { - using (_waitHandle) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Disposing..."); - _queue.Enqueue(null); - _waitHandle.Set(); - _worker.Join(); - } - } - - Disposed = true; - } - - /// - /// Finalizer - /// - ~GenericQueue() - { - Dispose(true); - } - - /// - /// Key - /// - public string Key - { - get { return _key; } + return int.MaxValue; } } + + /// + /// Returns the number of elements currently in the CrestronQueue + /// + public int QueueCount + { + get + { + return _queue.Count; + } + } + + /// + /// Constructor with no thread priority + /// + /// + public GenericQueue(string key) + : this(key, _defaultPriority, 0, 0) + { + } + + /// + /// Constructor with queue size + /// + /// + /// Fixed size for the queue to hold + public GenericQueue(string key, int capacity) + : this(key, _defaultPriority, capacity, 0) + { + } + + /// + /// Constructor for generic queue with no pacing + /// + /// Key + /// Pacing in ms between actions + public GenericQueue(int pacing, string key) + : this(key, _defaultPriority, 0, pacing) + { + } + + /// + /// Constructor with pacing and capacity + /// + /// + /// + /// + public GenericQueue(string key, int pacing, int capacity) + : this(key, _defaultPriority, capacity, pacing) + { + } + + /// + /// Constructor with pacing and priority + /// + /// + /// + /// + public GenericQueue(string key, int pacing, Thread.eThreadPriority priority) + : this(key, priority, 0, pacing) + { + } + + /// + /// Constructor with pacing, priority and capacity + /// + /// + /// + /// + public GenericQueue(string key, Thread.eThreadPriority priority, int capacity) + : this(key, priority, capacity, 0) + { + } + + /// + /// Constructor with pacing, priority and capacity + /// + /// + /// + /// + /// + public GenericQueue(string key, int pacing, Thread.eThreadPriority priority, int capacity) + : this(key, priority, capacity, pacing) + { + } + + /// + /// Constructor for generic queue with no pacing + /// + /// Key + /// + /// + /// + protected GenericQueue(string key, Thread.eThreadPriority priority, int capacity, int pacing) + { + _key = key; + int cap = 25; // sets default + if (capacity > 0) + { + cap = capacity; // overrides default + } + + _queue = new ConcurrentQueue(); + _worker = new Thread(ProcessQueue, null, Thread.eThreadStartOptions.Running) + { + Priority = priority, + Name = _key + }; + + SetDelayValues(pacing); + } + + private void SetDelayValues(int pacing) + { + _delayEnabled = pacing > 0; + _delayTime = pacing; + + CrestronEnvironment.ProgramStatusEventHandler += programEvent => + { + if (programEvent != eProgramStatusEventType.Stopping) + return; + + Dispose(true); + }; + } + + /// + /// Thread callback + /// + /// The action used to process dequeued items + /// Null when the thread is exited + private object ProcessQueue(object obj) + { + while (true) + { + if (_queue.TryDequeue(out var item) && item == null) + break; + + if (item != null) + { + try + { + //Debug.LogMessage(LogEventLevel.Verbose, this, "Processing queue item: '{0}'", item.ToString()); + item.Dispatch(); + + if (_delayEnabled) + Thread.Sleep(_delayTime); + } + catch (ThreadAbortException) + { + //swallowing this exception, as it should only happen on shut down + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Information, this, "Caught an exception in the Queue: {1}:{0}", ex.Message, ex); + Debug.LogMessage(LogEventLevel.Verbose, this, "Stack Trace: {0}", ex.StackTrace); + + if (ex.InnerException != null) + { + Debug.LogMessage(LogEventLevel.Information, this, "---\r\n{0}", ex.InnerException.Message); + Debug.LogMessage(LogEventLevel.Verbose, this, "Stack Trace: {0}", ex.InnerException.StackTrace); + } + } + } + else _waitHandle.Wait(); + } + + return null; + } + + public void Enqueue(IQueueMessage item) + { + if (Disposed) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Queue has been disposed. Enqueuing messages not allowed while program is stopping."); + return; + } + + _queue.Enqueue(item); + _waitHandle.Set(); + } + + /// + /// Disposes the thread and cleans up resources. Thread cannot be restarted once + /// disposed. + /// + public void Dispose() + { + Dispose(true); + CrestronEnvironment.GC.SuppressFinalize(this); + } + + /// + /// Actually does the disposing. If you override this method, be sure to either call the base implementation + /// or clean up all the resources yourself. + /// + /// set to true unless called from finalizer + protected void Dispose(bool disposing) + { + if (Disposed) + return; + + if (disposing) + { + using (_waitHandle) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Disposing..."); + _queue.Enqueue(null); + _waitHandle.Set(); + _worker.Join(); + } + } + + Disposed = true; + } + + ~GenericQueue() + { + Dispose(true); + } + + /// + /// Key + /// + public string Key + { + get { return _key; } + } } diff --git a/src/PepperDash.Essentials.Core/Queues/IQueue.cs b/src/PepperDash.Essentials.Core/Queues/IQueue.cs index a8cbbf6a..2eb6a5d1 100644 --- a/src/PepperDash.Essentials.Core/Queues/IQueue.cs +++ b/src/PepperDash.Essentials.Core/Queues/IQueue.cs @@ -1,22 +1,23 @@ using System; using PepperDash.Core; -namespace PepperDash.Essentials.Core.Queues +namespace PepperDash.Essentials.Core.Queues; + + +/// +/// Defines the contract for IQueue +/// +public interface IQueue : IKeyed, IDisposable where T : class { /// - /// Defines the contract for IQueue + /// Enqueues an item /// - public interface IQueue : IKeyed, IDisposable where T : class - { - /// - /// Enqueues an item - /// - /// item to be queued - void Enqueue(T item); + /// item to be queued + void Enqueue(T item); - /// - /// gets the disposed status of the queue - /// - bool Disposed { get; } - } + /// + /// gets the disposed status of the queue + /// + bool Disposed { get; } } + diff --git a/src/PepperDash.Essentials.Core/Queues/IQueueMessage.cs b/src/PepperDash.Essentials.Core/Queues/IQueueMessage.cs index 2a050a3b..a832cd46 100644 --- a/src/PepperDash.Essentials.Core/Queues/IQueueMessage.cs +++ b/src/PepperDash.Essentials.Core/Queues/IQueueMessage.cs @@ -1,15 +1,15 @@ using System; -namespace PepperDash.Essentials.Core.Queues +namespace PepperDash.Essentials.Core.Queues; + + +/// +/// Defines the contract for IQueueMessage +/// +public interface IQueueMessage { /// - /// Defines the contract for IQueueMessage + /// Dispatches the message /// - public interface IQueueMessage - { - /// - /// Dispatches the message - /// - void Dispatch(); - } -} \ No newline at end of file + void Dispatch(); +} diff --git a/src/PepperDash.Essentials.Core/Queues/ProcessStringMessage.cs b/src/PepperDash.Essentials.Core/Queues/ProcessStringMessage.cs index 5e2db7df..34fa8617 100644 --- a/src/PepperDash.Essentials.Core/Queues/ProcessStringMessage.cs +++ b/src/PepperDash.Essentials.Core/Queues/ProcessStringMessage.cs @@ -1,48 +1,48 @@ using System; -namespace PepperDash.Essentials.Core.Queues +namespace PepperDash.Essentials.Core.Queues; + +/// +/// Message class for processing strings via an IQueue +/// +public class ProcessStringMessage : IQueueMessage { + private readonly Action _action; + private readonly string _message; + /// /// Represents a ProcessStringMessage /// - public class ProcessStringMessage : IQueueMessage + /// Message to be processed + /// Action to invoke on the message + public ProcessStringMessage(string message, Action action) { - private readonly Action _action; - private readonly string _message; + _message = message; + _action = action; + } - /// - /// Constructor - /// - /// Message to be processed - /// Action to invoke on the message - public ProcessStringMessage(string message, Action action) - { - _message = message; - _action = action; - } + /// + /// Processes the string with the given action + /// + public void Dispatch() + { + if (_action == null || String.IsNullOrEmpty(_message)) + return; - /// - /// Processes the string with the given action - /// - public void Dispatch() - { - if (_action == null || String.IsNullOrEmpty(_message)) - return; + _action(_message); + } - _action(_message); - } - - /// - /// To string - /// - /// The current message - /// - /// ToString method - /// - /// - public override string ToString() - { - return _message ?? String.Empty; - } + /// + /// To string + /// + /// The current message + /// + /// ToString method + /// + /// + public override string ToString() + { + return _message ?? String.Empty; } } + diff --git a/src/PepperDash.Essentials.Core/Queues/StringResponseProcessor.cs b/src/PepperDash.Essentials.Core/Queues/StringResponseProcessor.cs index 207fd597..44c83e37 100644 --- a/src/PepperDash.Essentials.Core/Queues/StringResponseProcessor.cs +++ b/src/PepperDash.Essentials.Core/Queues/StringResponseProcessor.cs @@ -2,111 +2,104 @@ using Crestron.SimplSharp; using PepperDash.Core; -namespace PepperDash.Essentials.Core.Queues +namespace PepperDash.Essentials.Core.Queues; + +public sealed class StringResponseProcessor : IKeyed, IDisposable { - /// - /// Processes string responses from a communication port or gather using a queue to ensure thread safety - /// - public sealed class StringResponseProcessor : IKeyed, IDisposable + private readonly Action _processStringAction; + private readonly IQueue _queue; + private readonly IBasicCommunication _coms; + private readonly CommunicationGather _gather; + + private StringResponseProcessor(string key, Action processStringAction) { - private readonly Action _processStringAction; - private readonly IQueue _queue; - private readonly IBasicCommunication _coms; - private readonly CommunicationGather _gather; + _processStringAction = processStringAction; + _queue = new GenericQueue(key); - private StringResponseProcessor(string key, Action processStringAction) + CrestronEnvironment.ProgramStatusEventHandler += programEvent => { - _processStringAction = processStringAction; - _queue = new GenericQueue(key); - - CrestronEnvironment.ProgramStatusEventHandler += programEvent => - { - if (programEvent != eProgramStatusEventType.Stopping) - return; - - Dispose(); - }; - } - - /// - /// Constructor that builds an instance and subscribes to coms TextReceived for processing - /// - /// Com port to process strings from - /// Action to process the incoming strings - public StringResponseProcessor(IBasicCommunication coms, Action processStringAction) - : this(coms.Key, processStringAction) - { - _coms = coms; - coms.TextReceived += OnResponseReceived; - } - - /// - /// Constructor that builds an instance and subscribes to gather Line Received for processing - /// - /// Gather to process strings from - /// Action to process the incoming strings - public StringResponseProcessor(CommunicationGather gather, Action processStringAction) - : this(gather.Port.Key, processStringAction) - { - _gather = gather; - gather.LineReceived += OnResponseReceived; - } - - private void OnResponseReceived(object sender, GenericCommMethodReceiveTextArgs args) - { - _queue.Enqueue(new ProcessStringMessage(args.Text, _processStringAction)); - } - - /// - /// Key - /// - public string Key - { - get { return _queue.Key; } - } - - /// - /// Disposes the instance and cleans up resources. - /// - public void Dispose() - { - Dispose(true); - CrestronEnvironment.GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (Disposed) + if (programEvent != eProgramStatusEventType.Stopping) return; - if (disposing) + Dispose(); + }; + } + + /// + /// Constructor that builds an instance and subscribes to coms TextReceived for processing + /// + /// Com port to process strings from + /// Action to process the incoming strings + public StringResponseProcessor(IBasicCommunication coms, Action processStringAction) + : this(coms.Key, processStringAction) + { + _coms = coms; + coms.TextReceived += OnResponseReceived; + } + + /// + /// Constructor that builds an instance and subscribes to gather Line Received for processing + /// + /// Gather to process strings from + /// Action to process the incoming strings + public StringResponseProcessor(CommunicationGather gather, Action processStringAction) + : this(gather.Port.Key, processStringAction) + { + _gather = gather; + gather.LineReceived += OnResponseReceived; + } + + private void OnResponseReceived(object sender, GenericCommMethodReceiveTextArgs args) + { + _queue.Enqueue(new ProcessStringMessage(args.Text, _processStringAction)); + } + + /// + /// Key + /// + public string Key + { + get { return _queue.Key; } + } + + /// + /// Disposes the instance and cleans up resources. + /// + public void Dispose() + { + Dispose(true); + CrestronEnvironment.GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (Disposed) + return; + + if (disposing) + { + if (_coms != null) + _coms.TextReceived -= OnResponseReceived; + + if (_gather != null) { - if (_coms != null) - _coms.TextReceived -= OnResponseReceived; - - if (_gather != null) - { - _gather.LineReceived -= OnResponseReceived; - _gather.Stop(); - } - - _queue.Dispose(); + _gather.LineReceived -= OnResponseReceived; + _gather.Stop(); } - Disposed = true; + _queue.Dispose(); } - /// - /// Gets or sets the Disposed - /// - public bool Disposed { get; private set; } + Disposed = true; + } - /// - /// Finalizer - /// - ~StringResponseProcessor() - { - Dispose(false); - } + /// + /// If the instance has been disposed or not. If it has, you can not use it anymore + /// + public bool Disposed { get; private set; } + + ~StringResponseProcessor() + { + Dispose(false); } } diff --git a/src/PepperDash.Essentials.Core/Ramps and Increments/ActionIncrementer.cs b/src/PepperDash.Essentials.Core/Ramps and Increments/ActionIncrementer.cs index 544e7d94..ded7d9f3 100644 --- a/src/PepperDash.Essentials.Core/Ramps and Increments/ActionIncrementer.cs +++ b/src/PepperDash.Essentials.Core/Ramps and Increments/ActionIncrementer.cs @@ -6,134 +6,114 @@ using Crestron.SimplSharp; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// An incrementer that can use the values of some other object/primitive value to do its thing. +/// It uses an Action to set the value and a Func to get the value from whatever this is +/// attached to. +/// +public class ActionIncrementer { + public int ChangeAmount { get; set; } + public int MaxValue { get; set; } + public int MinValue { get; set; } + public uint RepeatDelay { get; set; } + public uint RepeatTime { get; set; } + + Action SetAction; + Func GetFunc; + CTimer Timer; + /// - /// An incrementer that can use the values of some other object/primitive value to do its thing. - /// It uses an Action to set the value and a Func to get the value from whatever this is - /// attached to. + /// /// - public class ActionIncrementer + /// + /// + /// + /// + /// + /// Action that will be called when this needs to set the destination value + /// Func that is called to get the current value + public ActionIncrementer(int changeAmount, int minValue, int maxValue, uint repeatDelay, uint repeatTime, Action setAction, Func getFunc) { - /// - /// The amount to change the value by each increment - /// - public int ChangeAmount { get; set; } + SetAction = setAction; + GetFunc = getFunc; + ChangeAmount = changeAmount; + MaxValue = maxValue; + MinValue = minValue; + RepeatDelay = repeatDelay; + RepeatTime = repeatTime; + } - /// - /// The maximum value for the incrementer - /// - public int MaxValue { get; set; } + /// + /// Starts incrementing cycle + /// + public void StartUp() + { + if (Timer != null) return; + Go(ChangeAmount); + } - /// - /// The minimum value for the incrementer - /// - public int MinValue { get; set; } + /// + /// Starts decrementing cycle + /// + public void StartDown() + { + if (Timer != null) return; + Go(-ChangeAmount); + } - /// - /// The delay before repeating starts - /// - public uint RepeatDelay { get; set; } + /// + /// Stops the repeat + /// + public void Stop() + { + if (Timer != null) + Timer.Stop(); + Timer = null; + } - /// - /// The time between repeats - /// - public uint RepeatTime { get; set; } + /// + /// Helper that does the work of setting new level, and starting repeat loop, checking against bounds first. + /// + /// + void Go(int change) + { + int currentLevel = GetFunc(); + // Fire once then pause + int newLevel = currentLevel + change; + bool atLimit = CheckLevel(newLevel, out newLevel); + SetAction(newLevel); - Action SetAction; - Func GetFunc; - CTimer Timer; + if (atLimit) // Don't go past end + Stop(); + else if (Timer == null) // Only enter the timer if it's not already running + Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime); + } - /// - /// - /// - /// - /// - /// - /// - /// - /// Action that will be called when this needs to set the destination value - /// Func that is called to get the current value - public ActionIncrementer(int changeAmount, int minValue, int maxValue, uint repeatDelay, uint repeatTime, Action setAction, Func getFunc) + /// + /// Helper to check a new level against min/max. Returns revised level if new level + /// will go out of bounds + /// + /// The level to check against bounds + /// Revised level if bounds are exceeded. Min or max + /// true if new level is at or past bounds + bool CheckLevel(int levelIn, out int levelOut) + { + bool isAtLimit = false; + if (levelIn > MaxValue) { - SetAction = setAction; - GetFunc = getFunc; - ChangeAmount = changeAmount; - MaxValue = maxValue; - MinValue = minValue; - RepeatDelay = repeatDelay; - RepeatTime = repeatTime; + levelOut = MaxValue; + isAtLimit = true; } - - /// - /// StartUp method - /// - public void StartUp() + else if (levelIn < MinValue) { - if (Timer != null) return; - Go(ChangeAmount); - } - - /// - /// Starts decrementing cycle - /// - public void StartDown() - { - if (Timer != null) return; - Go(-ChangeAmount); - } - - /// - /// Stops the repeat - /// - public void Stop() - { - if (Timer != null) - Timer.Stop(); - Timer = null; - } - - /// - /// Helper that does the work of setting new level, and starting repeat loop, checking against bounds first. - /// - /// - void Go(int change) - { - int currentLevel = GetFunc(); - // Fire once then pause - int newLevel = currentLevel + change; - bool atLimit = CheckLevel(newLevel, out newLevel); - SetAction(newLevel); - - if (atLimit) // Don't go past end - Stop(); - else if (Timer == null) // Only enter the timer if it's not already running - Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime); - } - - /// - /// Helper to check a new level against min/max. Returns revised level if new level - /// will go out of bounds - /// - /// The level to check against bounds - /// Revised level if bounds are exceeded. Min or max - /// true if new level is at or past bounds - bool CheckLevel(int levelIn, out int levelOut) - { - bool isAtLimit = false; - if (levelIn > MaxValue) - { - levelOut = MaxValue; - isAtLimit = true; - } - else if (levelIn < MinValue) - { - levelOut = MinValue; - isAtLimit = true; - } - else - levelOut = levelIn; - return isAtLimit; + levelOut = MinValue; + isAtLimit = true; } + else + levelOut = levelIn; + return isAtLimit; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Ramps and Increments/NumericalHelpers.cs b/src/PepperDash.Essentials.Core/Ramps and Increments/NumericalHelpers.cs index 383593f4..4feca569 100644 --- a/src/PepperDash.Essentials.Core/Ramps and Increments/NumericalHelpers.cs +++ b/src/PepperDash.Essentials.Core/Ramps and Increments/NumericalHelpers.cs @@ -4,40 +4,39 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + +/// +/// Helper methods for numerical operations +/// +public class NumericalHelpers +{ /// - /// Represents a NumericalHelpers + /// Scales a value /// - public class NumericalHelpers - { - /// - /// Scales a value - /// - /// - /// - /// - /// - /// - /// - public static double Scale(double input, double inMin, double inMax, double outMin, double outMax) + /// + /// + /// + /// + /// + /// + public static double Scale(double input, double inMin, double inMax, double outMin, double outMax) + { + //Debug.LogMessage(LogEventLevel.Verbose, this, "Scaling (double) input '{0}' with min '{1}'/max '{2}' to output range min '{3}'/max '{4}'", input, inMin, inMax, outMin, outMax); + + double inputRange = inMax - inMin; + + if (inputRange <= 0) { - //Debug.LogMessage(LogEventLevel.Verbose, this, "Scaling (double) input '{0}' with min '{1}'/max '{2}' to output range min '{3}'/max '{4}'", input, inMin, inMax, outMin, outMax); - - double inputRange = inMax - inMin; - - if (inputRange <= 0) - { - throw new ArithmeticException(string.Format("Invalid Input Range '{0}' for Scaling. Min '{1}' Max '{2}'.", inputRange, inMin, inMax)); - } - - double outputRange = outMax - outMin; - - var output = (((input - inMin) * outputRange) / inputRange) + outMin; - - // Debug.LogMessage(LogEventLevel.Verbose, this, "Scaled output '{0}'", output); - - return output; + throw new ArithmeticException(string.Format("Invalid Input Range '{0}' for Scaling. Min '{1}' Max '{2}'.", inputRange, inMin, inMax)); } + + double outputRange = outMax - outMin; + + var output = (((input - inMin) * outputRange) / inputRange) + outMin; + + // Debug.LogMessage(LogEventLevel.Verbose, this, "Scaled output '{0}'", output); + + return output; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Ramps and Increments/UshortSigIncrementer.cs b/src/PepperDash.Essentials.Core/Ramps and Increments/UshortSigIncrementer.cs index 02c40f69..7ee4c956 100644 --- a/src/PepperDash.Essentials.Core/Ramps and Increments/UshortSigIncrementer.cs +++ b/src/PepperDash.Essentials.Core/Ramps and Increments/UshortSigIncrementer.cs @@ -8,133 +8,93 @@ using Crestron.SimplSharpPro; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Attaches to UShortInputSig and does incremental ramping of the signal +/// +public class UshortSigIncrementer { - /// - /// Attaches to UShortInputSig and does incremental ramping of the signal - /// - public class UshortSigIncrementer + UShortInputSig TheSig; + public ushort ChangeAmount { get; set; } + public int MaxValue { get; set; } + public int MinValue { get; set; } + public uint RepeatDelay { get; set; } + public uint RepeatTime { get; set; } + bool SignedMode; + CTimer Timer; + + public UshortSigIncrementer(UShortInputSig sig, ushort changeAmount, int minValue, int maxValue, uint repeatDelay, uint repeatTime) { - UShortInputSig TheSig; + TheSig = sig; + ChangeAmount = changeAmount; + MaxValue = maxValue; + MinValue = minValue; + if (MinValue < 0 || MaxValue < 0) SignedMode = true; + RepeatDelay = repeatDelay; + RepeatTime = repeatTime; + if (SignedMode && (MinValue < -32768 || MaxValue > 32767)) + Debug.LogMessage(LogEventLevel.Debug, "UshortSigIncrementer has signed values that exceed range of -32768, 32767"); + } - /// - /// The amount to change the value by each increment - /// - public ushort ChangeAmount { get; set; } + public void StartUp() + { + if (Timer != null) return; + Go(ChangeAmount); + } - /// - /// The maximum value for the incrementer - /// - public int MaxValue { get; set; } + public void StartDown() + { + if (Timer != null) return; + Go(-ChangeAmount); + } - /// - /// The minimum value for the incrementer - /// - public int MinValue { get; set; } + void Go(int change) + { + int level; + if (SignedMode) level = TheSig.ShortValue; + else level = TheSig.UShortValue; - /// - /// The delay before repeating starts - /// - public uint RepeatDelay { get; set; } + // Fire once then pause + int newLevel = level + change; + bool atLimit = CheckLevel(newLevel, out newLevel); + SetValue((ushort)newLevel); - /// - /// The time between repeats - /// - public uint RepeatTime { get; set; } - bool SignedMode; - CTimer Timer; + if (atLimit) // Don't go past end + Stop(); + else if (Timer == null) // Only enter the timer if it's not already running + Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime); + } - /// - /// Constructor - /// - /// the signal toi be incremented - /// the amount to increment by - /// the minimum value of the signal - /// the maximum value of the signal - /// the delay before repeating starts - /// the time between repeats - public UshortSigIncrementer(UShortInputSig sig, ushort changeAmount, int minValue, int maxValue, uint repeatDelay, uint repeatTime) + bool CheckLevel(int levelIn, out int levelOut) + { + bool IsAtLimit = false; + if (levelIn > MaxValue) { - TheSig = sig; - ChangeAmount = changeAmount; - MaxValue = maxValue; - MinValue = minValue; - if (MinValue < 0 || MaxValue < 0) SignedMode = true; - RepeatDelay = repeatDelay; - RepeatTime = repeatTime; - if (SignedMode && (MinValue < -32768 || MaxValue > 32767)) - Debug.LogMessage(LogEventLevel.Debug, "UshortSigIncrementer has signed values that exceed range of -32768, 32767"); + levelOut = MaxValue; + IsAtLimit = true; } - - /// - /// StartUp method - /// - public void StartUp() + else if (levelIn < MinValue) { - if (Timer != null) return; - Go(ChangeAmount); + levelOut = MinValue; + IsAtLimit = true; } + else + levelOut = levelIn; + return IsAtLimit; + } - /// - /// StartDown method - /// - public void StartDown() - { - if (Timer != null) return; - Go(-ChangeAmount); - } + public void Stop() + { + if (Timer != null) + Timer.Stop(); + Timer = null; + } - void Go(int change) - { - int level; - if (SignedMode) level = TheSig.ShortValue; - else level = TheSig.UShortValue; - - // Fire once then pause - int newLevel = level + change; - bool atLimit = CheckLevel(newLevel, out newLevel); - SetValue((ushort)newLevel); - - - if (atLimit) // Don't go past end - Stop(); - else if (Timer == null) // Only enter the timer if it's not already running - Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime); - } - - bool CheckLevel(int levelIn, out int levelOut) - { - bool IsAtLimit = false; - if (levelIn > MaxValue) - { - levelOut = MaxValue; - IsAtLimit = true; - } - else if (levelIn < MinValue) - { - levelOut = MinValue; - IsAtLimit = true; - } - else - levelOut = levelIn; - return IsAtLimit; - } - - /// - /// Stop method - /// - public void Stop() - { - if (Timer != null) - Timer.Stop(); - Timer = null; - } - - void SetValue(ushort value) - { - //CrestronConsole.PrintLine("Increment level:{0} / {1}", value, (short)value); - TheSig.UShortValue = value; - } + void SetValue(ushort value) + { + //CrestronConsole.PrintLine("Increment level:{0} / {1}", value, (short)value); + TheSig.UShortValue = value; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs b/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs index 601c3559..81b5bf3b 100644 --- a/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs +++ b/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs @@ -16,613 +16,541 @@ using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Devices; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// A device that when linked to a room can power the room on when enabled during scheduled hours. +/// +public class RoomOnToDefaultSourceWhenOccupied : ReconfigurableDevice { - /// - /// A device that when linked to a room can power the room on when enabled during scheduled hours. - /// - public class RoomOnToDefaultSourceWhenOccupied : ReconfigurableDevice + RoomOnToDefaultSourceWhenOccupiedConfig PropertiesConfig; + + public bool FeatureEnabled { get; private set; } + + public DateTime FeatureEnabledTime { get; private set; } + + ScheduledEvent FeatureEnableEvent; + + const string FeatureEnableEventName = "EnableRoomOnToDefaultSourceWhenOccupied"; + + public DateTime FeatureDisabledTime { get; private set; } + + ScheduledEvent FeatureDisableEvent; + + const string FeatureDisableEventName = "DisableRoomOnToDefaultSourceWhenOccupied"; + + ScheduledEventGroup FeatureEventGroup; + + public IRoomOccupancy Room { get; private set; } + + private Fusion.IEssentialsRoomFusionController FusionRoom; + + public RoomOnToDefaultSourceWhenOccupied(DeviceConfig config) : + base (config) { - RoomOnToDefaultSourceWhenOccupiedConfig PropertiesConfig; + PropertiesConfig = JsonConvert.DeserializeObject(config.Properties.ToString()); - /// - /// Gets or sets the FeatureEnabled - /// - public bool FeatureEnabled { get; private set; } + FeatureEventGroup = new ScheduledEventGroup(this.Key); - /// - /// Gets or sets the FeatureEnabledTime - /// - public DateTime FeatureEnabledTime { get; private set; } + FeatureEventGroup.RetrieveAllEvents(); - ScheduledEvent FeatureEnableEvent; + // Add to the global class for tracking + Scheduler.AddEventGroup(FeatureEventGroup); - const string FeatureEnableEventName = "EnableRoomOnToDefaultSourceWhenOccupied"; - - /// - /// Gets or sets the FeatureDisabledTime - /// - public DateTime FeatureDisabledTime { get; private set; } - - ScheduledEvent FeatureDisableEvent; - - const string FeatureDisableEventName = "DisableRoomOnToDefaultSourceWhenOccupied"; - - ScheduledEventGroup FeatureEventGroup; - - /// - /// Gets or sets the Room - /// - public IRoomOccupancy Room { get; private set; } - - private Fusion.IEssentialsRoomFusionController FusionRoom; - - /// - /// Constructor for RoomOnToDefaultSourceWhenOccupied - /// - /// config of the device - public RoomOnToDefaultSourceWhenOccupied(DeviceConfig config) : - base (config) + AddPostActivationAction(() => { - PropertiesConfig = JsonConvert.DeserializeObject(config.Properties.ToString()); - - FeatureEventGroup = new ScheduledEventGroup(this.Key); - - FeatureEventGroup.RetrieveAllEvents(); - - // Add to the global class for tracking - Scheduler.AddEventGroup(FeatureEventGroup); - - AddPostActivationAction(() => - { - // Subscribe to room event to know when RoomOccupancy is set and ready to be subscribed to - if (Room != null) - Room.RoomOccupancyIsSet += new EventHandler(RoomOccupancyIsSet); - - else - Debug.LogMessage(LogEventLevel.Debug, this, "Room has no RoomOccupancy object set"); - - var fusionRoomKey = PropertiesConfig.RoomKey + "-fusion"; - - 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); - }); - } - - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() - { - SetUpDevice(); - - return base.CustomActivate(); - } - - /// - /// Sets up device based on config values - /// - void SetUpDevice() - { - Room = DeviceManager.GetDeviceForKey(PropertiesConfig.RoomKey) as IRoomOccupancy; - + // Subscribe to room event to know when RoomOccupancy is set and ready to be subscribed to if (Room != null) + Room.RoomOccupancyIsSet += new EventHandler(RoomOccupancyIsSet); + + else + Debug.LogMessage(LogEventLevel.Debug, this, "Room has no RoomOccupancy object set"); + + var fusionRoomKey = PropertiesConfig.RoomKey + "-fusion"; + + 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); + }); + } + + public override bool CustomActivate() + { + SetUpDevice(); + + return base.CustomActivate(); + } + + /// + /// Sets up device based on config values + /// + void SetUpDevice() + { + Room = DeviceManager.GetDeviceForKey(PropertiesConfig.RoomKey) as IRoomOccupancy; + + if (Room != null) + { + try { - try - { - FeatureEnabledTime = DateTime.Parse(PropertiesConfig.OccupancyStartTime); + FeatureEnabledTime = DateTime.Parse(PropertiesConfig.OccupancyStartTime); - if (FeatureEnabledTime != null) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Enabled Time: {0}", FeatureEnabledTime.ToString()); - } - else - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to parse {0} to DateTime", PropertiesConfig.OccupancyStartTime); - } - catch (Exception e) + if (FeatureEnabledTime != null) { - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to parse OccupancyStartTime property: {0} \n Error: {1}", PropertiesConfig.OccupancyStartTime, e); + Debug.LogMessage(LogEventLevel.Debug, this, "Enabled Time: {0}", FeatureEnabledTime.ToString()); } - - try - { - FeatureDisabledTime = DateTime.Parse(PropertiesConfig.OccupancyEndTime); - - if (FeatureDisabledTime != null) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Disabled Time: {0}", FeatureDisabledTime.ToString()); - } - else - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to parse {0} to DateTime", PropertiesConfig.OccupancyEndTime); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to parse a DateTime config value \n Error: {1}", e); - } - - if (!PropertiesConfig.EnableRoomOnWhenOccupied) - FeatureEventGroup.ClearAllEvents(); else - { - AddEnableEventToGroup(); - - AddDisableEventToGroup(); - - FeatureEventGroup.UserGroupCallBack += new ScheduledEventGroup.UserEventGroupCallBack(FeatureEventGroup_UserGroupCallBack); - - FeatureEventGroup.EnableAllEvents(); - } - - FeatureEnabled = CheckIfFeatureShouldBeEnabled(); + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to parse {0} to DateTime", PropertiesConfig.OccupancyStartTime); } - else - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to get room from Device Manager with key: {0}", PropertiesConfig.RoomKey); - } - - /// - /// CustomSetConfig method - /// - /// config of the device - protected override void CustomSetConfig(DeviceConfig config) - { - var newPropertiesConfig = JsonConvert.DeserializeObject(config.Properties.ToString()); - - if(newPropertiesConfig != null) - PropertiesConfig = newPropertiesConfig; - - ConfigWriter.UpdateDeviceConfig(config); - - SetUpDevice(); - } - - /// - /// Subscribe to feedback from RoomIsOccupiedFeedback on Room - /// - /// - /// - void RoomOccupancyIsSet(object sender, EventArgs e) - { - if (Room.RoomOccupancy != null) + catch (Exception e) { - Room.RoomOccupancy.RoomIsOccupiedFeedback.OutputChange -= RoomIsOccupiedFeedback_OutputChange; - Room.RoomOccupancy.RoomIsOccupiedFeedback.OutputChange += new EventHandler(RoomIsOccupiedFeedback_OutputChange); - Debug.LogMessage(LogEventLevel.Debug, this, "Subscribed to RoomOccupancy status from: '{0}'", Room.Key); - } - } - - void FeatureEventGroup_UserGroupCallBack(ScheduledEvent SchEvent, ScheduledEventCommon.eCallbackReason type) - { - Debug.LogMessage(LogEventLevel.Debug, this, "{0}:{1} @ {2}", SchEvent.Name, type, DateTime.Now); - - if (type == ScheduledEventCommon.eCallbackReason.NormalExpiration) - { - SchEvent.Acknowledge(); - - if (SchEvent.Name == FeatureEnableEventName) - { - - if (PropertiesConfig.EnableRoomOnWhenOccupied) - FeatureEnabled = true; - - Debug.LogMessage(LogEventLevel.Debug, this, "*****Feature Enabled by event.*****"); - } - else if (SchEvent.Name == FeatureDisableEventName) - { - FeatureEnabled = false; - - Debug.LogMessage(LogEventLevel.Debug, this, "*****Feature Disabled by event.*****"); - } - } - } - - /// - /// Checks if the feature should be currently enabled. Used on startup if processor starts after start time but before end time - /// - /// - bool CheckIfFeatureShouldBeEnabled() - { - bool enabled = false; - - if(PropertiesConfig.EnableRoomOnWhenOccupied) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Current Time: {0} \n FeatureEnabledTime: {1} \n FeatureDisabledTime: {2}", DateTime.Now, FeatureEnabledTime, FeatureDisabledTime); - - if (DateTime.Now.TimeOfDay.CompareTo(FeatureEnabledTime.TimeOfDay) >= 0 && FeatureDisabledTime.TimeOfDay.CompareTo(DateTime.Now.TimeOfDay) > 0) - { - if (SchedulerUtilities.CheckIfDayOfWeekMatchesRecurrenceDays(DateTime.Now, CalculateDaysOfWeekRecurrence())) - { - enabled = true; - } - } + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to parse OccupancyStartTime property: {0} \n Error: {1}", PropertiesConfig.OccupancyStartTime, e); } - if(enabled) - Debug.LogMessage(LogEventLevel.Debug, this, "*****Feature Enabled*****"); - else - Debug.LogMessage(LogEventLevel.Debug, this, "*****Feature Disabled*****"); - - return enabled; - } - - /// - /// Respond to Occupancy status event - /// - /// - /// - void RoomIsOccupiedFeedback_OutputChange(object sender, FeedbackEventArgs e) - { - Debug.LogMessage(LogEventLevel.Debug, this, "RoomIsOccupiedFeeback.OutputChange event fired. e.BoolValue: {0}", e.BoolValue); - if(e.BoolValue) + try { - // Occupancy detected + FeatureDisabledTime = DateTime.Parse(PropertiesConfig.OccupancyEndTime); - if (FeatureEnabled) + if (FeatureDisabledTime != null) { - var essentialsRoom = Room as IEssentialsRoom; - - if (essentialsRoom != null) { - if (!essentialsRoom.OnFeedback.BoolValue) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Powering Room on to default source"); - - var defaultRouteRoom = Room as IRunDefaultPresentRoute; - - if (defaultRouteRoom != null) - { - defaultRouteRoom.RunDefaultPresentRoute(); - } - } - } - // Check room power state first - + Debug.LogMessage(LogEventLevel.Debug, this, "Disabled Time: {0}", FeatureDisabledTime.ToString()); } - } - } - - void CreateEvent(ScheduledEvent schEvent, string name) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Adding Event: '{0}'", name); - // Create the event - if (schEvent == null) - schEvent = new ScheduledEvent(name, FeatureEventGroup); - - // Set up its initial properties - - schEvent.Acknowledgeable = false; - - if(!schEvent.Persistent) - schEvent.Persistent = true; - - schEvent.DateAndTime.SetFirstDayOfWeek(ScheduledEventCommon.eFirstDayOfWeek.Sunday); - - // Set config driven properties - - if (schEvent.Name == FeatureEnableEventName) - { - schEvent.Description = "Enables the RoomOnToDefaultSourceWhenOccupiedFeature"; - - var eventRecurrennce = CalculateDaysOfWeekRecurrence(); - - var eventTime = new DateTime(); - - // Check to make sure the date for this event is in the future - if (DateTime.Now.CompareTo(FeatureEnabledTime) > 0) - eventTime = FeatureEnabledTime.AddDays(1); else - eventTime = FeatureEnabledTime; - - Debug.LogMessage(LogEventLevel.Debug, this, "eventTime (before recurrence check): {0}", eventTime); - - // Check day of week against recurrence days and move date ahead as necessary to avoid throwing an exception by trying to set the event - // start date on a day of the week that doesn't match teh recurrence values - while(!SchedulerUtilities.CheckIfDayOfWeekMatchesRecurrenceDays(eventTime, eventRecurrennce)) - { - eventTime = eventTime.AddDays(1); - Debug.LogMessage(LogEventLevel.Debug, this, "eventTime does not fall on a recurrence weekday. eventTime: {0}", eventTime); - } - - schEvent.DateAndTime.SetAbsoluteEventTime(eventTime); - - Debug.LogMessage(LogEventLevel.Debug, this, "Event '{0}' Absolute time set to {1}", schEvent.Name, schEvent.DateAndTime.ToString()); - - //CalculateAndSetAcknowledgeExpirationTimeout(schEvent, FeatureEnabledTime, FeatureDisabledTime); - - schEvent.Recurrence.Weekly(eventRecurrennce); - + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to parse {0} to DateTime", PropertiesConfig.OccupancyEndTime); } - else if (schEvent.Name == FeatureDisableEventName) + catch (Exception e) { - schEvent.Description = "Disables the RoomOnToDefaultSourceWhenOccupiedFeature"; - - // Check to make sure the date for this event is in the future - if (DateTime.Now.CompareTo(FeatureDisabledTime) > 0) - schEvent.DateAndTime.SetAbsoluteEventTime(FeatureDisabledTime.AddDays(1)); - else - schEvent.DateAndTime.SetAbsoluteEventTime(FeatureDisabledTime); - - Debug.LogMessage(LogEventLevel.Debug, this, "Event '{0}' Absolute time set to {1}", schEvent.Name, schEvent.DateAndTime.ToString()); - - CalculateAndSetAcknowledgeExpirationTimeout(schEvent, FeatureDisabledTime, FeatureEnabledTime); - - schEvent.Recurrence.Daily(); + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to parse a DateTime config value \n Error: {1}", e); } - } - void CalculateAndSetAcknowledgeExpirationTimeout(ScheduledEvent schEvent, DateTime time1, DateTime time2) - { - Debug.LogMessage(LogEventLevel.Debug, this, "time1.Hour = {0}", time1.Hour); - Debug.LogMessage(LogEventLevel.Debug, this, "time2.Hour = {0}", time2.Hour); - Debug.LogMessage(LogEventLevel.Debug, this, "time1.Minute = {0}", time1.Minute); - Debug.LogMessage(LogEventLevel.Debug, this, "time2.Minute = {0}", time2.Minute); - - // Calculate the Acknowledge Expiration timer to be the time between the enable and dispable events, less one minute - var ackHours = time2.Hour - time1.Hour; - if(ackHours < 0) - ackHours = ackHours + 24; - var ackMinutes = time2.Minute - time1.Minute; - - Debug.LogMessage(LogEventLevel.Debug, this, "ackHours = {0}, ackMinutes = {1}", ackHours, ackMinutes); - - var ackTotalMinutes = ((ackHours * 60) + ackMinutes) - 1; - - var ackExpHour = ackTotalMinutes / 60; - var ackExpMinutes = ackTotalMinutes % 60; - - Debug.LogMessage(LogEventLevel.Debug, this, "Acknowledge Expiration Timeout: {0} hours, {1} minutes", ackExpHour, ackExpMinutes); - - schEvent.AcknowledgeExpirationTimeout.Hour = (ushort)(ackHours); - schEvent.AcknowledgeExpirationTimeout.Minute = (ushort)(ackExpMinutes); - } - - /// - /// Checks existing event to see if it matches the execution time - /// - /// event we are checking - /// time we are checking against - /// - bool CheckExistingEventTimeForMatch(ScheduledEvent existingEvent, DateTime newTime) - { - bool isMatch = true; - - // Check to see if hour and minute match - if (existingEvent.DateAndTime.Hour != newTime.Hour || existingEvent.DateAndTime.Minute != newTime.Minute) - return false; - - - return isMatch; - } - - /// - /// Checks existing event to see if it matches the recurrence days - /// - /// - /// - /// - bool CheckExistingEventRecurrenceForMatch(ScheduledEvent existingEvent, ScheduledEventCommon.eWeekDays eWeekdays) - { - bool isMatch = true; - - // Check to see if recurrence matches - if (eWeekdays != existingEvent.Recurrence.RecurrenceDays) - return false; - - return isMatch; - } - - /// - /// Adds the Enable event to the local event group and sets its properties based on config - /// - void AddEnableEventToGroup() - { - if (!FeatureEventGroup.ScheduledEvents.ContainsKey(FeatureEnableEventName)) - { - CreateEvent(FeatureEnableEvent, FeatureEnableEventName); - } + if (!PropertiesConfig.EnableRoomOnWhenOccupied) + FeatureEventGroup.ClearAllEvents(); else { - // Check if existing event has same time and recurrence as config values + AddEnableEventToGroup(); - FeatureEnableEvent = FeatureEventGroup.ScheduledEvents[FeatureEnableEventName]; - Debug.LogMessage(LogEventLevel.Debug, this, "Enable event already found in group"); + AddDisableEventToGroup(); - // Check config times and days against DateAndTime of existing event. If different, delete existing event and create new event - if(!CheckExistingEventTimeForMatch(FeatureEnableEvent, FeatureEnabledTime) || !CheckExistingEventRecurrenceForMatch(FeatureEnableEvent, CalculateDaysOfWeekRecurrence())) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Existing event does not match new config properties. Deleting exisiting event: '{0}'", FeatureEnableEvent.Name); - FeatureEventGroup.DeleteEvent(FeatureEnableEvent); + FeatureEventGroup.UserGroupCallBack += new ScheduledEventGroup.UserEventGroupCallBack(FeatureEventGroup_UserGroupCallBack); - FeatureEnableEvent = null; - - CreateEvent(FeatureEnableEvent, FeatureEnableEventName); - } + FeatureEventGroup.EnableAllEvents(); } + FeatureEnabled = CheckIfFeatureShouldBeEnabled(); } + else + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to get room from Device Manager with key: {0}", PropertiesConfig.RoomKey); + } - /// - /// Adds the Enable event to the local event group and sets its properties based on config - /// - void AddDisableEventToGroup() + + protected override void CustomSetConfig(DeviceConfig config) + { + var newPropertiesConfig = JsonConvert.DeserializeObject(config.Properties.ToString()); + + if(newPropertiesConfig != null) + PropertiesConfig = newPropertiesConfig; + + ConfigWriter.UpdateDeviceConfig(config); + + SetUpDevice(); + } + + /// + /// Subscribe to feedback from RoomIsOccupiedFeedback on Room + /// + /// + /// + void RoomOccupancyIsSet(object sender, EventArgs e) + { + if (Room.RoomOccupancy != null) { - if (!FeatureEventGroup.ScheduledEvents.ContainsKey(FeatureDisableEventName)) - { - CreateEvent(FeatureDisableEvent, FeatureDisableEventName); - } - else - { - FeatureDisableEvent = FeatureEventGroup.ScheduledEvents[FeatureDisableEventName]; - Debug.LogMessage(LogEventLevel.Debug, this, "Disable event already found in group"); - - // Check config times against DateAndTime of existing event. If different, delete existing event and create new event - if(!CheckExistingEventTimeForMatch(FeatureDisableEvent, FeatureDisabledTime)) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Existing event does not match new config properties. Deleting exisiting event: '{0}'", FeatureDisableEvent.Name); - - FeatureEventGroup.DeleteEvent(FeatureDisableEvent); - - FeatureDisableEvent = null; - - CreateEvent(FeatureDisableEvent, FeatureDisableEventName); - } - } + Room.RoomOccupancy.RoomIsOccupiedFeedback.OutputChange -= RoomIsOccupiedFeedback_OutputChange; + Room.RoomOccupancy.RoomIsOccupiedFeedback.OutputChange += new EventHandler(RoomIsOccupiedFeedback_OutputChange); + Debug.LogMessage(LogEventLevel.Debug, this, "Subscribed to RoomOccupancy status from: '{0}'", Room.Key); } + } + void FeatureEventGroup_UserGroupCallBack(ScheduledEvent SchEvent, ScheduledEventCommon.eCallbackReason type) + { + Debug.LogMessage(LogEventLevel.Debug, this, "{0}:{1} @ {2}", SchEvent.Name, type, DateTime.Now); - /// - /// Calculates the correct bitfield enum value for the event recurrence based on the config values - /// - /// - ScheduledEventCommon.eWeekDays CalculateDaysOfWeekRecurrence() + if (type == ScheduledEventCommon.eCallbackReason.NormalExpiration) { - ScheduledEventCommon.eWeekDays value = new ScheduledEventCommon.eWeekDays(); + SchEvent.Acknowledge(); - if (PropertiesConfig.EnableSunday) - value = value | ScheduledEventCommon.eWeekDays.Sunday; - if (PropertiesConfig.EnableMonday) - value = value | ScheduledEventCommon.eWeekDays.Monday; - if (PropertiesConfig.EnableTuesday) - value = value | ScheduledEventCommon.eWeekDays.Tuesday; - if (PropertiesConfig.EnableWednesday) - value = value | ScheduledEventCommon.eWeekDays.Wednesday; - if (PropertiesConfig.EnableThursday) - value = value | ScheduledEventCommon.eWeekDays.Thursday; - if (PropertiesConfig.EnableFriday) - value = value | ScheduledEventCommon.eWeekDays.Friday; - if (PropertiesConfig.EnableSaturday) - value = value | ScheduledEventCommon.eWeekDays.Saturday; - - return value; - } - - /// - /// Callback for event that enables feature. Enables feature if config property is true - /// - /// - /// - void FeatureEnableEvent_UserCallBack(ScheduledEvent SchEvent, ScheduledEventCommon.eCallbackReason type) - { - if (type == ScheduledEventCommon.eCallbackReason.NormalExpiration) + if (SchEvent.Name == FeatureEnableEventName) { - if(PropertiesConfig.EnableRoomOnWhenOccupied) + + if (PropertiesConfig.EnableRoomOnWhenOccupied) FeatureEnabled = true; - Debug.LogMessage(LogEventLevel.Debug, this, "RoomOnToDefaultSourceWhenOccupied Feature Enabled."); + Debug.LogMessage(LogEventLevel.Debug, this, "*****Feature Enabled by event.*****"); } - } - - /// - /// Callback for event that enables feature. Disables feature - /// - /// - /// - void FeatureDisableEvent_UserCallBack(ScheduledEvent SchEvent, ScheduledEventCommon.eCallbackReason type) - { - if (type == ScheduledEventCommon.eCallbackReason.NormalExpiration) + else if (SchEvent.Name == FeatureDisableEventName) { FeatureEnabled = false; - Debug.LogMessage(LogEventLevel.Debug, this, "RoomOnToDefaultSourceWhenOccupied Feature Disabled."); + Debug.LogMessage(LogEventLevel.Debug, this, "*****Feature Disabled by event.*****"); } } } /// - /// Represents a RoomOnToDefaultSourceWhenOccupiedConfig + /// Checks if the feature should be currently enabled. Used on startup if processor starts after start time but before end time /// - public class RoomOnToDefaultSourceWhenOccupiedConfig + /// + bool CheckIfFeatureShouldBeEnabled() { - /// - /// Gets or sets the RoomKey - /// - [JsonProperty("roomKey")] - public string RoomKey { get; set; } + bool enabled = false; - /// - /// Gets or sets the EnableRoomOnWhenOccupied - /// - [JsonProperty("enableRoomOnWhenOccupied")] - public bool EnableRoomOnWhenOccupied { get; set; } + if(PropertiesConfig.EnableRoomOnWhenOccupied) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Current Time: {0} \n FeatureEnabledTime: {1} \n FeatureDisabledTime: {2}", DateTime.Now, FeatureEnabledTime, FeatureDisabledTime); - /// - /// Gets or sets the OccupancyStartTime - /// - [JsonProperty("occupancyStartTime")] - public string OccupancyStartTime { get; set; } + if (DateTime.Now.TimeOfDay.CompareTo(FeatureEnabledTime.TimeOfDay) >= 0 && FeatureDisabledTime.TimeOfDay.CompareTo(DateTime.Now.TimeOfDay) > 0) + { + if (SchedulerUtilities.CheckIfDayOfWeekMatchesRecurrenceDays(DateTime.Now, CalculateDaysOfWeekRecurrence())) + { + enabled = true; + } + } + } - /// - /// Gets or sets the OccupancyEndTime - /// - [JsonProperty("occupancyEndTime")] - public string OccupancyEndTime { get; set; } + if(enabled) + Debug.LogMessage(LogEventLevel.Debug, this, "*****Feature Enabled*****"); + else + Debug.LogMessage(LogEventLevel.Debug, this, "*****Feature Disabled*****"); - /// - /// Gets or sets the EnableSunday - /// - [JsonProperty("enableSunday")] - public bool EnableSunday { get; set; } - - /// - /// Gets or sets the EnableMonday - /// - [JsonProperty("enableMonday")] - public bool EnableMonday { get; set; } - - /// - /// Gets or sets the EnableTuesday - /// - [JsonProperty("enableWednesday")] - public bool EnableTuesday { get; set; } - - /// - /// Gets or sets the EnableWednesday - /// - [JsonProperty("enableWednesday")] - public bool EnableWednesday { get; set; } - - /// - /// Gets or sets the EnableThursday - /// - [JsonProperty("enableThursday")] - public bool EnableThursday { get; set; } - - /// - /// Gets or sets the EnableFriday - /// - [JsonProperty("enableFriday")] - public bool EnableFriday { get; set; } - - /// - /// Gets or sets the EnableSaturday - /// - [JsonProperty("enableSaturday")] - public bool EnableSaturday { get; set; } + return enabled; } /// - /// Represents a RoomOnToDefaultSourceWhenOccupiedFactory + /// Respond to Occupancy status event /// - public class RoomOnToDefaultSourceWhenOccupiedFactory : EssentialsDeviceFactory + /// + /// + void RoomIsOccupiedFeedback_OutputChange(object sender, FeedbackEventArgs e) { - /// - /// Constructor for RoomOnToDefaultSourceWhenOccupiedFactory - /// - public RoomOnToDefaultSourceWhenOccupiedFactory() + Debug.LogMessage(LogEventLevel.Debug, this, "RoomIsOccupiedFeeback.OutputChange event fired. e.BoolValue: {0}", e.BoolValue); + if(e.BoolValue) { - TypeNames = new List() { "roomonwhenoccupancydetectedfeature" }; - } + // Occupancy detected - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new RoomOnToDefaultSourceWhenOccupied Device"); - return new RoomOnToDefaultSourceWhenOccupied(dc); + if (FeatureEnabled) + { + var essentialsRoom = Room as IEssentialsRoom; + + if (essentialsRoom != null) { + if (!essentialsRoom.OnFeedback.BoolValue) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Powering Room on to default source"); + + var defaultRouteRoom = Room as IRunDefaultPresentRoute; + + if (defaultRouteRoom != null) + { + defaultRouteRoom.RunDefaultPresentRoute(); + } + } + } + // Check room power state first + + } } } + void CreateEvent(ScheduledEvent schEvent, string name) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Adding Event: '{0}'", name); + // Create the event + if (schEvent == null) + schEvent = new ScheduledEvent(name, FeatureEventGroup); + + // Set up its initial properties + + schEvent.Acknowledgeable = false; + + if(!schEvent.Persistent) + schEvent.Persistent = true; + + schEvent.DateAndTime.SetFirstDayOfWeek(ScheduledEventCommon.eFirstDayOfWeek.Sunday); + + // Set config driven properties + + if (schEvent.Name == FeatureEnableEventName) + { + schEvent.Description = "Enables the RoomOnToDefaultSourceWhenOccupiedFeature"; + + var eventRecurrennce = CalculateDaysOfWeekRecurrence(); + + var eventTime = new DateTime(); + + // Check to make sure the date for this event is in the future + if (DateTime.Now.CompareTo(FeatureEnabledTime) > 0) + eventTime = FeatureEnabledTime.AddDays(1); + else + eventTime = FeatureEnabledTime; + + Debug.LogMessage(LogEventLevel.Debug, this, "eventTime (before recurrence check): {0}", eventTime); + + // Check day of week against recurrence days and move date ahead as necessary to avoid throwing an exception by trying to set the event + // start date on a day of the week that doesn't match teh recurrence values + while(!SchedulerUtilities.CheckIfDayOfWeekMatchesRecurrenceDays(eventTime, eventRecurrennce)) + { + eventTime = eventTime.AddDays(1); + Debug.LogMessage(LogEventLevel.Debug, this, "eventTime does not fall on a recurrence weekday. eventTime: {0}", eventTime); + } + + schEvent.DateAndTime.SetAbsoluteEventTime(eventTime); + + Debug.LogMessage(LogEventLevel.Debug, this, "Event '{0}' Absolute time set to {1}", schEvent.Name, schEvent.DateAndTime.ToString()); + + //CalculateAndSetAcknowledgeExpirationTimeout(schEvent, FeatureEnabledTime, FeatureDisabledTime); + + schEvent.Recurrence.Weekly(eventRecurrennce); + + } + else if (schEvent.Name == FeatureDisableEventName) + { + schEvent.Description = "Disables the RoomOnToDefaultSourceWhenOccupiedFeature"; + + // Check to make sure the date for this event is in the future + if (DateTime.Now.CompareTo(FeatureDisabledTime) > 0) + schEvent.DateAndTime.SetAbsoluteEventTime(FeatureDisabledTime.AddDays(1)); + else + schEvent.DateAndTime.SetAbsoluteEventTime(FeatureDisabledTime); + + Debug.LogMessage(LogEventLevel.Debug, this, "Event '{0}' Absolute time set to {1}", schEvent.Name, schEvent.DateAndTime.ToString()); + + CalculateAndSetAcknowledgeExpirationTimeout(schEvent, FeatureDisabledTime, FeatureEnabledTime); + + schEvent.Recurrence.Daily(); + } + } + + void CalculateAndSetAcknowledgeExpirationTimeout(ScheduledEvent schEvent, DateTime time1, DateTime time2) + { + Debug.LogMessage(LogEventLevel.Debug, this, "time1.Hour = {0}", time1.Hour); + Debug.LogMessage(LogEventLevel.Debug, this, "time2.Hour = {0}", time2.Hour); + Debug.LogMessage(LogEventLevel.Debug, this, "time1.Minute = {0}", time1.Minute); + Debug.LogMessage(LogEventLevel.Debug, this, "time2.Minute = {0}", time2.Minute); + + // Calculate the Acknowledge Expiration timer to be the time between the enable and dispable events, less one minute + var ackHours = time2.Hour - time1.Hour; + if(ackHours < 0) + ackHours = ackHours + 24; + var ackMinutes = time2.Minute - time1.Minute; + + Debug.LogMessage(LogEventLevel.Debug, this, "ackHours = {0}, ackMinutes = {1}", ackHours, ackMinutes); + + var ackTotalMinutes = ((ackHours * 60) + ackMinutes) - 1; + + var ackExpHour = ackTotalMinutes / 60; + var ackExpMinutes = ackTotalMinutes % 60; + + Debug.LogMessage(LogEventLevel.Debug, this, "Acknowledge Expiration Timeout: {0} hours, {1} minutes", ackExpHour, ackExpMinutes); + + schEvent.AcknowledgeExpirationTimeout.Hour = (ushort)(ackHours); + schEvent.AcknowledgeExpirationTimeout.Minute = (ushort)(ackExpMinutes); + } + + /// + /// Checks existing event to see if it matches the execution time + /// + /// + /// + bool CheckExistingEventTimeForMatch(ScheduledEvent existingEvent, DateTime newTime) + { + bool isMatch = true; + + // Check to see if hour and minute match + if (existingEvent.DateAndTime.Hour != newTime.Hour || existingEvent.DateAndTime.Minute != newTime.Minute) + return false; + + + return isMatch; + } + + /// + /// Checks existing event to see if it matches the recurrence days + /// + /// + /// + /// + bool CheckExistingEventRecurrenceForMatch(ScheduledEvent existingEvent, ScheduledEventCommon.eWeekDays eWeekdays) + { + bool isMatch = true; + + // Check to see if recurrence matches + if (eWeekdays != existingEvent.Recurrence.RecurrenceDays) + return false; + + return isMatch; + } + + /// + /// Adds the Enable event to the local event group and sets its properties based on config + /// + void AddEnableEventToGroup() + { + if (!FeatureEventGroup.ScheduledEvents.ContainsKey(FeatureEnableEventName)) + { + CreateEvent(FeatureEnableEvent, FeatureEnableEventName); + } + else + { + // Check if existing event has same time and recurrence as config values + + FeatureEnableEvent = FeatureEventGroup.ScheduledEvents[FeatureEnableEventName]; + Debug.LogMessage(LogEventLevel.Debug, this, "Enable event already found in group"); + + // Check config times and days against DateAndTime of existing event. If different, delete existing event and create new event + if(!CheckExistingEventTimeForMatch(FeatureEnableEvent, FeatureEnabledTime) || !CheckExistingEventRecurrenceForMatch(FeatureEnableEvent, CalculateDaysOfWeekRecurrence())) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Existing event does not match new config properties. Deleting exisiting event: '{0}'", FeatureEnableEvent.Name); + FeatureEventGroup.DeleteEvent(FeatureEnableEvent); + + FeatureEnableEvent = null; + + CreateEvent(FeatureEnableEvent, FeatureEnableEventName); + } + } + + } + + /// + /// Adds the Enable event to the local event group and sets its properties based on config + /// + void AddDisableEventToGroup() + { + if (!FeatureEventGroup.ScheduledEvents.ContainsKey(FeatureDisableEventName)) + { + CreateEvent(FeatureDisableEvent, FeatureDisableEventName); + } + else + { + FeatureDisableEvent = FeatureEventGroup.ScheduledEvents[FeatureDisableEventName]; + Debug.LogMessage(LogEventLevel.Debug, this, "Disable event already found in group"); + + // Check config times against DateAndTime of existing event. If different, delete existing event and create new event + if(!CheckExistingEventTimeForMatch(FeatureDisableEvent, FeatureDisabledTime)) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Existing event does not match new config properties. Deleting exisiting event: '{0}'", FeatureDisableEvent.Name); + + FeatureEventGroup.DeleteEvent(FeatureDisableEvent); + + FeatureDisableEvent = null; + + CreateEvent(FeatureDisableEvent, FeatureDisableEventName); + } + } + } + + + /// + /// Calculates the correct bitfield enum value for the event recurrence based on the config values + /// + /// + ScheduledEventCommon.eWeekDays CalculateDaysOfWeekRecurrence() + { + ScheduledEventCommon.eWeekDays value = new ScheduledEventCommon.eWeekDays(); + + if (PropertiesConfig.EnableSunday) + value = value | ScheduledEventCommon.eWeekDays.Sunday; + if (PropertiesConfig.EnableMonday) + value = value | ScheduledEventCommon.eWeekDays.Monday; + if (PropertiesConfig.EnableTuesday) + value = value | ScheduledEventCommon.eWeekDays.Tuesday; + if (PropertiesConfig.EnableWednesday) + value = value | ScheduledEventCommon.eWeekDays.Wednesday; + if (PropertiesConfig.EnableThursday) + value = value | ScheduledEventCommon.eWeekDays.Thursday; + if (PropertiesConfig.EnableFriday) + value = value | ScheduledEventCommon.eWeekDays.Friday; + if (PropertiesConfig.EnableSaturday) + value = value | ScheduledEventCommon.eWeekDays.Saturday; + + return value; + } + + /// + /// Callback for event that enables feature. Enables feature if config property is true + /// + /// + /// + void FeatureEnableEvent_UserCallBack(ScheduledEvent SchEvent, ScheduledEventCommon.eCallbackReason type) + { + if (type == ScheduledEventCommon.eCallbackReason.NormalExpiration) + { + if(PropertiesConfig.EnableRoomOnWhenOccupied) + FeatureEnabled = true; + + Debug.LogMessage(LogEventLevel.Debug, this, "RoomOnToDefaultSourceWhenOccupied Feature Enabled."); + } + } + + /// + /// Callback for event that enables feature. Disables feature + /// + /// + /// + void FeatureDisableEvent_UserCallBack(ScheduledEvent SchEvent, ScheduledEventCommon.eCallbackReason type) + { + if (type == ScheduledEventCommon.eCallbackReason.NormalExpiration) + { + FeatureEnabled = false; + + Debug.LogMessage(LogEventLevel.Debug, this, "RoomOnToDefaultSourceWhenOccupied Feature Disabled."); + } + } +} + +public class RoomOnToDefaultSourceWhenOccupiedConfig +{ + [JsonProperty("roomKey")] + public string RoomKey { get; set; } + + [JsonProperty("enableRoomOnWhenOccupied")] + public bool EnableRoomOnWhenOccupied { get; set; } + + [JsonProperty("occupancyStartTime")] + public string OccupancyStartTime { get; set; } + + [JsonProperty("occupancyEndTime")] + public string OccupancyEndTime { get; set; } + + [JsonProperty("enableSunday")] + public bool EnableSunday { get; set; } + + [JsonProperty("enableMonday")] + public bool EnableMonday { get; set; } + + [JsonProperty("enableTuesday")] + public bool EnableTuesday { get; set; } + + [JsonProperty("enableWednesday")] + public bool EnableWednesday { get; set; } + + [JsonProperty("enableThursday")] + public bool EnableThursday { get; set; } + + [JsonProperty("enableFriday")] + public bool EnableFriday { get; set; } + + [JsonProperty("enableSaturday")] + public bool EnableSaturday { get; set; } +} + +public class RoomOnToDefaultSourceWhenOccupiedFactory : EssentialsDeviceFactory +{ + public RoomOnToDefaultSourceWhenOccupiedFactory() + { + TypeNames = new List() { "roomonwhenoccupancydetectedfeature" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new RoomOnToDefaultSourceWhenOccupied Device"); + return new RoomOnToDefaultSourceWhenOccupied(dc); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs index a45adb27..3a9a7409 100644 --- a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs +++ b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs @@ -8,491 +8,481 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a device that manages room combinations by controlling partitions and scenarios. +/// +/// The allows for dynamic configuration of room +/// combinations based on partition states and predefined scenarios. It supports both automatic and manual modes +/// for managing room combinations. In automatic mode, the device determines the current room combination scenario +/// based on partition sensor states. In manual mode, scenarios can be set explicitly by the user. +public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner { + private EssentialsRoomCombinerPropertiesConfig _propertiesConfig; + + private IRoomCombinationScenario _currentScenario; + + private List _rooms; + /// - /// Represents a device that manages room combinations by controlling partitions and scenarios. + /// Gets a list of rooms represented as key-name pairs. /// - /// The allows for dynamic configuration of room - /// combinations based on partition states and predefined scenarios. It supports both automatic and manual modes - /// for managing room combinations. In automatic mode, the device determines the current room combination scenario - /// based on partition sensor states. In manual mode, scenarios can be set explicitly by the user. - public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner + public List Rooms { - private EssentialsRoomCombinerPropertiesConfig _propertiesConfig; - - private IRoomCombinationScenario _currentScenario; - - private List _rooms; - - /// - /// Gets a list of rooms represented as key-name pairs. - /// - public List Rooms + get { - get - { - return _rooms.Cast().ToList(); - } + return _rooms.Cast().ToList(); } + } - private bool _isInAutoMode; + private bool _isInAutoMode; - /// - /// Gets or sets a value indicating whether the system is operating in automatic mode. - /// - /// Changing this property triggers an update event via - /// IsInAutoModeFeedback.FireUpdate(). Ensure that any event listeners are properly configured to handle - /// this update. - public bool IsInAutoMode + /// + /// Gets or sets a value indicating whether the system is operating in automatic mode. + /// + /// Changing this property triggers an update event via + /// IsInAutoModeFeedback.FireUpdate(). Ensure that any event listeners are properly configured to handle + /// this update. + public bool IsInAutoMode + { + get { - get - { - return _isInAutoMode; - } - set - { - if (value == _isInAutoMode) - { - return; - } - - _isInAutoMode = value; - IsInAutoModeFeedback.FireUpdate(); - } + return _isInAutoMode; } - - /// - /// Gets a value indicating whether automatic mode is disabled. - /// - public bool DisableAutoMode + set { - get + if (value == _isInAutoMode) { - return _propertiesConfig.DisableAutoMode; - } - } - - private CTimer _scenarioChangeDebounceTimer; - - private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s - - private Mutex _scenarioChange = new Mutex(); - - /// - /// Initializes a new instance of the class, which manages room combination - /// scenarios and partition states. - /// - /// The class is designed to handle dynamic room - /// combination scenarios based on partition states. It supports both automatic and manual modes for managing - /// room combinations. By default, the instance starts in automatic mode unless the - /// specifies otherwise. After activation, the room combiner initializes partition state providers and sets up - /// the initial room configuration. Additionally, it subscribes to the event to ensure proper initialization of dependent devices - /// before determining or setting the room combination scenario. - /// The unique identifier for the room combiner instance. - /// The configuration properties for the room combiner, including default settings and debounce times. - public EssentialsRoomCombiner(string key, EssentialsRoomCombinerPropertiesConfig props) - : base(key) - { - _propertiesConfig = props; - - Partitions = new List(); - RoomCombinationScenarios = new List(); - - if (_propertiesConfig.ScenarioChangeDebounceTimeSeconds > 0) - { - _scenarioChangeDebounceTimeSeconds = _propertiesConfig.ScenarioChangeDebounceTimeSeconds; - } - - IsInAutoModeFeedback = new BoolFeedback(() => _isInAutoMode); - - // default to auto mode - IsInAutoMode = true; - - if (_propertiesConfig.defaultToManualMode) - { - IsInAutoMode = false; - } - - IsInAutoModeFeedback.FireUpdate(); - - CreateScenarios(); - - AddPostActivationAction(() => - { - SetupPartitionStateProviders(); - - SetRooms(); - }); - - - // Subscribe to the AllDevicesInitialized event - // We need to wait until all devices are initialized in case - // any actions are dependent on 3rd party devices already being - // connected and initialized - DeviceManager.AllDevicesInitialized += (o, a) => - { - if (IsInAutoMode) - { - DetermineRoomCombinationScenario(); - } - else - { - SetRoomCombinationScenario(_propertiesConfig.defaultScenarioKey); - } - }; - } - - private void CreateScenarios() - { - foreach (var scenarioConfig in _propertiesConfig.Scenarios) - { - var scenario = new RoomCombinationScenario(scenarioConfig); - RoomCombinationScenarios.Add(scenario); - } - } - - private void SetRooms() - { - _rooms = new List(); - - foreach (var roomKey in _propertiesConfig.RoomKeys) - { - var room = DeviceManager.GetDeviceForKey(roomKey); - - if (DeviceManager.GetDeviceForKey(roomKey) is IEssentialsRoom essentialsRoom) - { - _rooms.Add(essentialsRoom); - } - } - - var rooms = DeviceManager.AllDevices.OfType().Cast(); - - foreach (var room in rooms) - { - room.Deactivate(); - } - } - - private void SetupPartitionStateProviders() - { - foreach (var pConfig in _propertiesConfig.Partitions) - { - var sensor = DeviceManager.GetDeviceForKey(pConfig.DeviceKey) as IPartitionStateProvider; - - var partition = new EssentialsPartitionController(pConfig.Key, pConfig.Name, sensor, _propertiesConfig.defaultToManualMode, pConfig.AdjacentRoomKeys); - - partition.PartitionPresentFeedback.OutputChange += PartitionPresentFeedback_OutputChange; - - Partitions.Add(partition); - } - } - - private void PartitionPresentFeedback_OutputChange(object sender, FeedbackEventArgs e) - { - StartDebounceTimer(); - } - - private void StartDebounceTimer() - { - // default to 500ms for manual mode - var time = 500; - - // if in auto mode, debounce the scenario change - if (IsInAutoMode) time = _scenarioChangeDebounceTimeSeconds * 1000; - - if (_scenarioChangeDebounceTimer == null) - { - _scenarioChangeDebounceTimer = new CTimer((o) => DetermineRoomCombinationScenario(), time); - } - else - { - _scenarioChangeDebounceTimer.Reset(time); - } - } - - /// - /// Determines the current room combination scenario based on the state of the partition sensors - /// - private void DetermineRoomCombinationScenario() - { - if (_scenarioChangeDebounceTimer != null) - { - _scenarioChangeDebounceTimer.Dispose(); - _scenarioChangeDebounceTimer = null; - } - - this.LogInformation("Determining Combination Scenario"); - - var currentScenario = RoomCombinationScenarios.FirstOrDefault((s) => - { - this.LogDebug("Checking scenario {scenarioKey}", s.Key); - // iterate the partition states - foreach (var partitionState in s.PartitionStates) - { - this.LogDebug("checking PartitionState {partitionStateKey}", partitionState.PartitionKey); - // get the partition by key - var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionState.PartitionKey)); - - this.LogDebug("Expected State: {partitionPresent} Actual State: {partitionState}", partitionState.PartitionPresent, partition.PartitionPresentFeedback.BoolValue); - - if (partition != null && partitionState.PartitionPresent != partition.PartitionPresentFeedback.BoolValue) - { - // the partition can't be found or the state doesn't match - return false; - } - } - // if it hasn't returned false by now we have the matching scenario - return true; - }); - - if (currentScenario != null) - { - this.LogInformation("Found combination Scenario {scenarioKey}", currentScenario.Key); - ChangeScenario(currentScenario); - } - } - - private async Task ChangeScenario(IRoomCombinationScenario newScenario) - { - - - if (newScenario == _currentScenario) - { - return; - } - - // Deactivate the old scenario first - if (_currentScenario != null) - { - Debug.LogMessage(LogEventLevel.Information, "Deactivating scenario {currentScenario}", this, _currentScenario.Name); - await _currentScenario.Deactivate(); - } - - _currentScenario = newScenario; - - // Activate the new scenario - if (_currentScenario != null) - { - Debug.LogMessage(LogEventLevel.Debug, $"Current Scenario: {_currentScenario.Name}", this); - await _currentScenario.Activate(); - } - - RoomCombinationScenarioChanged?.Invoke(this, new EventArgs()); - - - } - - #region IEssentialsRoomCombiner Members - - /// - /// Occurs when the room combination scenario changes. - /// - /// This event is triggered whenever the configuration or state of the room combination - /// changes. Subscribers can use this event to update their logic or UI based on the new scenario. - public event EventHandler RoomCombinationScenarioChanged; - - /// - /// Gets the current room combination scenario. - /// - public IRoomCombinationScenario CurrentScenario - { - get - { - return _currentScenario; - } - } - - /// - /// Gets or sets the IsInAutoModeFeedback - /// - public BoolFeedback IsInAutoModeFeedback { get; private set; } - - /// - /// Enables auto mode for the room combiner and its partitions, allowing automatic room combination scenarios to - /// be determined. - /// - /// Auto mode allows the room combiner to automatically adjust its configuration based on - /// the state of its partitions. If auto mode is disabled in the configuration, this method logs a warning and - /// does not enable auto mode. - public void SetAutoMode() - { - if(_propertiesConfig.DisableAutoMode) - { - this.LogWarning("Auto mode is disabled for this room combiner. Cannot set to auto mode."); return; } - IsInAutoMode = true; - foreach (var partition in Partitions) - { - partition.SetAutoMode(); - } + _isInAutoMode = value; + IsInAutoModeFeedback.FireUpdate(); + } + } - DetermineRoomCombinationScenario(); + /// + /// Gets a value indicating whether automatic mode is disabled. + /// + public bool DisableAutoMode + { + get + { + return _propertiesConfig.DisableAutoMode; + } + } + + private CTimer _scenarioChangeDebounceTimer; + + private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s + + private Mutex _scenarioChange = new Mutex(); + + /// + /// Initializes a new instance of the class, which manages room combination + /// scenarios and partition states. + /// + /// The class is designed to handle dynamic room + /// combination scenarios based on partition states. It supports both automatic and manual modes for managing + /// room combinations. By default, the instance starts in automatic mode unless the + /// specifies otherwise. After activation, the room combiner initializes partition state providers and sets up + /// the initial room configuration. Additionally, it subscribes to the event to ensure proper initialization of dependent devices + /// before determining or setting the room combination scenario. + /// The unique identifier for the room combiner instance. + /// The configuration properties for the room combiner, including default settings and debounce times. + public EssentialsRoomCombiner(string key, EssentialsRoomCombinerPropertiesConfig props) + : base(key) + { + _propertiesConfig = props; + + Partitions = new List(); + RoomCombinationScenarios = new List(); + + if (_propertiesConfig.ScenarioChangeDebounceTimeSeconds > 0) + { + _scenarioChangeDebounceTimeSeconds = _propertiesConfig.ScenarioChangeDebounceTimeSeconds; } - /// - /// Switches the system to manual mode, disabling automatic operations. - /// - /// This method sets the system to manual mode by updating the mode state and propagates - /// the change to all partitions. Once in manual mode, automatic operations are disabled for the system and its - /// partitions. - /// - /// SetManualMode method - /// - public void SetManualMode() + IsInAutoModeFeedback = new BoolFeedback(() => _isInAutoMode); + + // default to auto mode + IsInAutoMode = true; + + if (_propertiesConfig.defaultToManualMode) { IsInAutoMode = false; - - foreach (var partition in Partitions) - { - partition.SetManualMode(); - } } - /// - /// Toggles the current mode between automatic and manual. - /// - /// If the current mode is automatic, this method switches to manual mode. If the - /// current mode is manual, it switches to automatic mode. - /// - /// ToggleMode method - /// - public void ToggleMode() + IsInAutoModeFeedback.FireUpdate(); + + CreateScenarios(); + + AddPostActivationAction(() => + { + SetupPartitionStateProviders(); + + SetRooms(); + }); + + + // Subscribe to the AllDevicesInitialized event + // We need to wait until all devices are initialized in case + // any actions are dependent on 3rd party devices already being + // connected and initialized + DeviceManager.AllDevicesInitialized += (o, a) => { if (IsInAutoMode) { - SetManualMode(); + DetermineRoomCombinationScenario(); } else { - SetAutoMode(); + SetRoomCombinationScenario(_propertiesConfig.defaultScenarioKey); + } + }; + } + + private void CreateScenarios() + { + foreach (var scenarioConfig in _propertiesConfig.Scenarios) + { + var scenario = new RoomCombinationScenario(scenarioConfig); + RoomCombinationScenarios.Add(scenario); + } + } + + private void SetRooms() + { + _rooms = new List(); + + foreach (var roomKey in _propertiesConfig.RoomKeys) + { + var room = DeviceManager.GetDeviceForKey(roomKey); + + if (DeviceManager.GetDeviceForKey(roomKey) is IEssentialsRoom essentialsRoom) + { + _rooms.Add(essentialsRoom); } } - /// - /// Gets or sets the RoomCombinationScenarios - /// - public List RoomCombinationScenarios { get; private set; } + var rooms = DeviceManager.AllDevices.OfType().Cast(); - /// - /// Gets the collection of partition controllers managed by this instance. - /// - public List Partitions { get; private set; } - - /// - /// Toggles the state of the partition identified by the specified partition key. - /// - /// If no partition with the specified key exists, the method performs no - /// action. - /// The key of the partition whose state is to be toggled. This value cannot be null or empty. - public void TogglePartitionState(string partitionKey) + foreach (var room in rooms) { - var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionKey)); + room.Deactivate(); + } + } - if (partition != null) - { - partition.ToggglePartitionState(); - } + private void SetupPartitionStateProviders() + { + foreach (var pConfig in _propertiesConfig.Partitions) + { + var sensor = DeviceManager.GetDeviceForKey(pConfig.DeviceKey) as IPartitionStateProvider; + + var partition = new EssentialsPartitionController(pConfig.Key, pConfig.Name, sensor, _propertiesConfig.defaultToManualMode, pConfig.AdjacentRoomKeys); + + partition.PartitionPresentFeedback.OutputChange += PartitionPresentFeedback_OutputChange; + + Partitions.Add(partition); + } + } + + private void PartitionPresentFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + StartDebounceTimer(); + } + + private void StartDebounceTimer() + { + // default to 500ms for manual mode + var time = 500; + + // if in auto mode, debounce the scenario change + if (IsInAutoMode) time = _scenarioChangeDebounceTimeSeconds * 1000; + + if (_scenarioChangeDebounceTimer == null) + { + _scenarioChangeDebounceTimer = new CTimer((o) => DetermineRoomCombinationScenario(), time); + } + else + { + _scenarioChangeDebounceTimer.Reset(time); + } + } + + /// + /// Determines the current room combination scenario based on the state of the partition sensors + /// + private void DetermineRoomCombinationScenario() + { + if (_scenarioChangeDebounceTimer != null) + { + _scenarioChangeDebounceTimer.Dispose(); + _scenarioChangeDebounceTimer = null; } - /// - /// Sets the room combination scenario based on the specified scenario key. - /// - /// This method manually adjusts the partition states according to the specified - /// scenario. If the application is in auto mode, the operation will not proceed, and a log message will be - /// generated indicating that the mode must be set to manual first. If the specified scenario key does not - /// match any existing scenario, a debug log message will be generated. For each partition state in the - /// scenario, the corresponding partition will be updated to either "Present" or "Not Present" based on the - /// scenario's configuration. If a partition key in the scenario cannot be found, a debug log message will be - /// generated. - /// The key identifying the room combination scenario to apply. This must match the key of an existing scenario. - public void SetRoomCombinationScenario(string scenarioKey) + this.LogInformation("Determining Combination Scenario"); + + var currentScenario = RoomCombinationScenarios.FirstOrDefault((s) => { - if (IsInAutoMode) + this.LogDebug("Checking scenario {scenarioKey}", s.Key); + // iterate the partition states + foreach (var partitionState in s.PartitionStates) + { + this.LogDebug("checking PartitionState {partitionStateKey}", partitionState.PartitionKey); + // get the partition by key + var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionState.PartitionKey)); + + this.LogDebug("Expected State: {partitionPresent} Actual State: {partitionState}", partitionState.PartitionPresent, partition.PartitionPresentFeedback.BoolValue); + + if (partition != null && partitionState.PartitionPresent != partition.PartitionPresentFeedback.BoolValue) + { + // the partition can't be found or the state doesn't match + return false; + } + } + // if it hasn't returned false by now we have the matching scenario + return true; + }); + + if (currentScenario != null) + { + this.LogInformation("Found combination Scenario {scenarioKey}", currentScenario.Key); + ChangeScenario(currentScenario); + } + } + + private async Task ChangeScenario(IRoomCombinationScenario newScenario) + { + + + if (newScenario == _currentScenario) { - Debug.LogMessage(LogEventLevel.Information, this, "Cannot set room combination scenario when in auto mode. Set to auto mode first."); return; } - // Get the scenario - var scenario = RoomCombinationScenarios.FirstOrDefault((s) => s.Key.Equals(scenarioKey)); - - - // Set the parition states from the scenario manually - if (scenario != null) + // Deactivate the old scenario first + if (_currentScenario != null) { - Debug.LogMessage(LogEventLevel.Information, this, "Manually setting scenario to '{0}'", scenario.Key); - foreach (var partitionState in scenario.PartitionStates) - { - var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionState.PartitionKey)); + Debug.LogMessage(LogEventLevel.Information, "Deactivating scenario {currentScenario}", this, _currentScenario.Name); + await _currentScenario.Deactivate(); + } - if (partition != null) + _currentScenario = newScenario; + + // Activate the new scenario + if (_currentScenario != null) + { + Debug.LogMessage(LogEventLevel.Debug, $"Current Scenario: {_currentScenario.Name}", this); + await _currentScenario.Activate(); + } + + RoomCombinationScenarioChanged?.Invoke(this, new EventArgs()); + + + } + + #region IEssentialsRoomCombiner Members + + /// + /// Occurs when the room combination scenario changes. + /// + /// This event is triggered whenever the configuration or state of the room combination + /// changes. Subscribers can use this event to update their logic or UI based on the new scenario. + public event EventHandler RoomCombinationScenarioChanged; + + /// + /// Gets the current room combination scenario. + /// + public IRoomCombinationScenario CurrentScenario + { + get + { + return _currentScenario; + } + } + + /// + /// Gets the feedback indicating whether the system is currently in auto mode. + /// + public BoolFeedback IsInAutoModeFeedback { get; private set; } + + /// + /// Enables auto mode for the room combiner and its partitions, allowing automatic room combination scenarios to + /// be determined. + /// + /// Auto mode allows the room combiner to automatically adjust its configuration based on + /// the state of its partitions. If auto mode is disabled in the configuration, this method logs a warning and + /// does not enable auto mode. + public void SetAutoMode() + { + if(_propertiesConfig.DisableAutoMode) + { + this.LogWarning("Auto mode is disabled for this room combiner. Cannot set to auto mode."); + return; + } + IsInAutoMode = true; + + foreach (var partition in Partitions) + { + partition.SetAutoMode(); + } + + DetermineRoomCombinationScenario(); + } + + /// + /// Switches the system to manual mode, disabling automatic operations. + /// + /// This method sets the system to manual mode by updating the mode state and propagates + /// the change to all partitions. Once in manual mode, automatic operations are disabled for the system and its + /// partitions. + public void SetManualMode() + { + IsInAutoMode = false; + + foreach (var partition in Partitions) + { + partition.SetManualMode(); + } + } + + /// + /// Toggles the current mode between automatic and manual. + /// + /// If the current mode is automatic, this method switches to manual mode. If the + /// current mode is manual, it switches to automatic mode. + public void ToggleMode() + { + if (IsInAutoMode) + { + SetManualMode(); + } + else + { + SetAutoMode(); + } + } + + /// + /// Gets the collection of room combination scenarios. + /// + public List RoomCombinationScenarios { get; private set; } + + /// + /// Gets the collection of partition controllers managed by this instance. + /// + public List Partitions { get; private set; } + + /// + /// Toggles the state of the partition identified by the specified partition key. + /// + /// If no partition with the specified key exists, the method performs no + /// action. + /// The key of the partition whose state is to be toggled. This value cannot be null or empty. + public void TogglePartitionState(string partitionKey) + { + var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionKey)); + + if (partition != null) + { + partition.ToggglePartitionState(); + } + } + + /// + /// Sets the room combination scenario based on the specified scenario key. + /// + /// This method manually adjusts the partition states according to the specified + /// scenario. If the application is in auto mode, the operation will not proceed, and a log message will be + /// generated indicating that the mode must be set to manual first. If the specified scenario key does not + /// match any existing scenario, a debug log message will be generated. For each partition state in the + /// scenario, the corresponding partition will be updated to either "Present" or "Not Present" based on the + /// scenario's configuration. If a partition key in the scenario cannot be found, a debug log message will be + /// generated. + /// The key identifying the room combination scenario to apply. This must match the key of an existing scenario. + public void SetRoomCombinationScenario(string scenarioKey) + { + if (IsInAutoMode) + { + Debug.LogMessage(LogEventLevel.Information, this, "Cannot set room combination scenario when in auto mode. Set to auto mode first."); + return; + } + + // Get the scenario + var scenario = RoomCombinationScenarios.FirstOrDefault((s) => s.Key.Equals(scenarioKey)); + + + // Set the parition states from the scenario manually + if (scenario != null) + { + Debug.LogMessage(LogEventLevel.Information, this, "Manually setting scenario to '{0}'", scenario.Key); + foreach (var partitionState in scenario.PartitionStates) + { + var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionState.PartitionKey)); + + if (partition != null) + { + if (partitionState.PartitionPresent) { - if (partitionState.PartitionPresent) - { - Debug.LogMessage(LogEventLevel.Information, this, "Manually setting state to Present for: '{0}'", partition.Key); - partition.SetPartitionStatePresent(); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Manually setting state to Not Present for: '{0}'", partition.Key); - partition.SetPartitionStateNotPresent(); - } + Debug.LogMessage(LogEventLevel.Information, this, "Manually setting state to Present for: '{0}'", partition.Key); + partition.SetPartitionStatePresent(); } else { - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to find partition with key: '{0}'", partitionState.PartitionKey); + Debug.LogMessage(LogEventLevel.Information, this, "Manually setting state to Not Present for: '{0}'", partition.Key); + partition.SetPartitionStateNotPresent(); } } - } - else - { - Debug.LogMessage(LogEventLevel.Debug, this, "Unable to find scenario with key: '{0}'", scenarioKey); + else + { + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to find partition with key: '{0}'", partitionState.PartitionKey); + } } } + else + { + Debug.LogMessage(LogEventLevel.Debug, this, "Unable to find scenario with key: '{0}'", scenarioKey); + } + } - #endregion + #endregion +} + +/// +/// Provides a factory for creating instances of devices. +/// +/// This factory is responsible for constructing devices +/// based on the provided configuration. It supports the type name "essentialsroomcombiner" for device +/// creation. +public class EssentialsRoomCombinerFactory : EssentialsDeviceFactory +{ + /// + /// Initializes a new instance of the class. + /// + /// This factory is used to create instances of room combiners with the specified type + /// names. By default, the factory includes the type name "essentialsroomcombiner". + public EssentialsRoomCombinerFactory() + { + TypeNames = new List { "essentialsroomcombiner" }; } /// - /// Provides a factory for creating instances of devices. + /// Creates and initializes a new instance of the device. /// - /// This factory is responsible for constructing devices - /// based on the provided configuration. It supports the type name "essentialsroomcombiner" for device - /// creation. - /// - /// Represents a EssentialsRoomCombinerFactory - /// - public class EssentialsRoomCombinerFactory : EssentialsDeviceFactory + /// This method uses the provided device configuration to extract the properties and + /// create an device. Ensure that the configuration contains valid + /// properties for the device to be created successfully. + /// The device configuration containing the key and properties required to build the device. + /// A new instance of initialized with the specified configuration. + public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.DeviceConfig dc) { - /// - /// Initializes a new instance of the class. - /// - /// This factory is used to create instances of room combiners with the specified type - /// names. By default, the factory includes the type name "essentialsroomcombiner". - public EssentialsRoomCombinerFactory() - { - TypeNames = new List { "essentialsroomcombiner" }; - } + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new EssentialsRoomCombiner Device"); - /// - /// Creates and initializes a new instance of the device. - /// - /// This method uses the provided device configuration to extract the properties and - /// create an device. Ensure that the configuration contains valid - /// properties for the device to be created successfully. - /// The device configuration containing the key and properties required to build the device. - /// A new instance of initialized with the specified configuration. - public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new EssentialsRoomCombiner Device"); + var props = dc.Properties.ToObject(); - var props = dc.Properties.ToObject(); - - return new EssentialsRoomCombiner(dc.Key, props); - } + return new EssentialsRoomCombiner(dc.Key, props); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs index 868f3992..a8e9ae97 100644 --- a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs @@ -1,3 +1,4 @@ + using System.Collections.Generic; using PepperDash.Core; @@ -11,10 +12,10 @@ namespace PepperDash.Essentials.Core /// public class EssentialsRoomCombinerPropertiesConfig { - /// - /// Gets or sets a value indicating whether the system operates in automatic mode. - /// Some systems don't have partitions sensors, and show shouldn't allow auto mode to be turned on. When this is true in the configuration, - /// auto mode won't be allowed to be turned on. + /// + /// Gets or sets a value indicating whether the system operates in automatic mode. + /// Some systems don't have partitions sensors, and show shouldn't allow auto mode to be turned on. When this is true in the configuration, + /// auto mode won't be allowed to be turned on. /// [JsonProperty("disableAutoMode")] public bool DisableAutoMode { get; set; } @@ -49,8 +50,8 @@ namespace PepperDash.Essentials.Core [JsonProperty("defaultScenarioKey")] public string defaultScenarioKey { get; set; } - /// - /// Gets or sets the debounce time, in seconds, for scenario changes. + /// + /// Gets or sets the debounce time, in seconds, for scenario changes. /// [JsonProperty("scenarioChangeDebounceTimeSeconds")] public int ScenarioChangeDebounceTimeSeconds { get; set; } @@ -61,14 +62,14 @@ namespace PepperDash.Essentials.Core /// public class PartitionConfig : IKeyName { - /// - /// Gets or sets the unique key associated with the object. + /// + /// Gets or sets the unique key associated with the object. /// [JsonProperty("key")] public string Key { get; set; } - /// - /// Gets or sets the name associated with the object. + /// + /// Gets or sets the name associated with the object. /// [JsonProperty("name")] public string Name { get; set; } @@ -91,26 +92,26 @@ namespace PepperDash.Essentials.Core /// public class RoomCombinationScenarioConfig : IKeyName { - /// - /// Gets or sets the key associated with the object. + /// + /// Gets or sets the key associated with the object. /// [JsonProperty("key")] public string Key { get; set; } - /// - /// Gets or sets the name associated with the object. + /// + /// Gets or sets the name associated with the object. /// [JsonProperty("name")] public string Name { get; set; } - /// - /// Gets or sets a value indicating whether to hide this scenario in the UI. + /// + /// Gets or sets a value indicating whether to hide this scenario in the UI. /// [JsonProperty("hideInUi", NullValueHandling = NullValueHandling.Ignore)] - public bool HideInUi { get; set; } - - /// - /// Gets or sets the collection of partition states. + public bool HideInUi { get; set; } + + /// + /// Gets or sets the collection of partition states. /// [JsonProperty("partitionStates")] public List PartitionStates { get; set; } @@ -121,14 +122,14 @@ namespace PepperDash.Essentials.Core [JsonProperty("uiMap")] public Dictionary UiMap { get; set; } - /// - /// Gets or sets the list of actions to be performed during device activation. + /// + /// Gets or sets the list of actions to be performed during device activation. /// [JsonProperty("activationActions")] public List ActivationActions { get; set; } - /// - /// Gets or sets the list of actions to be performed when a device is deactivated. + /// + /// Gets or sets the list of actions to be performed when a device is deactivated. /// [JsonProperty("deactivationActions")] public List DeactivationActions { get; set; } @@ -139,16 +140,16 @@ namespace PepperDash.Essentials.Core /// public class PartitionState { - /// - /// Gets or sets the partition key used to group and organize data within a storage system. + /// + /// Gets or sets the partition key used to group and organize data within a storage system. /// [JsonProperty("partitionKey")] public string PartitionKey { get; set; } - /// - /// Gets or sets a value indicating whether a partition is currently present. + /// + /// Gets or sets a value indicating whether a partition is currently present. /// [JsonProperty("partitionSensedState")] public bool PartitionPresent { get; set; } } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsDualDisplayRoomPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsDualDisplayRoomPropertiesConfig.cs index 83584a5f..6bda3ac9 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsDualDisplayRoomPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsDualDisplayRoomPropertiesConfig.cs @@ -1,11 +1,10 @@  -namespace PepperDash.Essentials.Room.Config -{ - /// - /// Represents a EssentialsDualDisplayRoomPropertiesConfig - /// - public class EssentialsDualDisplayRoomPropertiesConfig : EssentialsNDisplayRoomPropertiesConfig - { +namespace PepperDash.Essentials.Room.Config; + +/// +/// Represents the configuration properties for a dual display room. +/// +public class EssentialsDualDisplayRoomPropertiesConfig : EssentialsNDisplayRoomPropertiesConfig +{ - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsHuddleRoomPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsHuddleRoomPropertiesConfig.cs index 85f8ca16..abd2bce6 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsHuddleRoomPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsHuddleRoomPropertiesConfig.cs @@ -1,34 +1,33 @@ using Newtonsoft.Json; -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + +/// +/// Configuration class for huddle room properties +/// +public class EssentialsHuddleRoomPropertiesConfig : EssentialsRoomPropertiesConfig { /// - /// Represents a EssentialsHuddleRoomPropertiesConfig + /// The key of the default display device /// - public class EssentialsHuddleRoomPropertiesConfig : EssentialsRoomPropertiesConfig - { - /// - /// The key of the default display device - /// - [JsonProperty("defaultDisplayKey")] - public string DefaultDisplayKey { get; set; } + [JsonProperty("defaultDisplayKey")] + public string DefaultDisplayKey { get; set; } - /// - /// The key of the default audio device - /// - [JsonProperty("defaultAudioKey")] - public string DefaultAudioKey { get; set; } + /// + /// The key of the default audio device + /// + [JsonProperty("defaultAudioKey")] + public string DefaultAudioKey { get; set; } - /// - /// The key of the source list for the room - /// - [JsonProperty("sourceListKey")] - public string SourceListKey { get; set; } + /// + /// The key of the source list for the room + /// + [JsonProperty("sourceListKey")] + public string SourceListKey { get; set; } - /// - /// The key of the default source item from the source list - /// - [JsonProperty("defaultSourceItem")] - public string DefaultSourceItem { get; set; } - } + /// + /// The key of the default source item from the source list + /// + [JsonProperty("defaultSourceItem")] + public string DefaultSourceItem { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsHuddleVtc1PropertiesConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsHuddleVtc1PropertiesConfig.cs index c41ed3c2..94de1d46 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsHuddleVtc1PropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsHuddleVtc1PropertiesConfig.cs @@ -1,19 +1,16 @@  using Newtonsoft.Json; -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + +/// +/// Represents a EssentialsHuddleVtc1PropertiesConfig +/// +public class EssentialsHuddleVtc1PropertiesConfig : EssentialsConferenceRoomPropertiesConfig { - /// - /// Represents a EssentialsHuddleVtc1PropertiesConfig + /// Gets or sets the DefaultDisplayKey /// - public class EssentialsHuddleVtc1PropertiesConfig : EssentialsConferenceRoomPropertiesConfig - { - /// - /// Gets or sets the DefaultDisplayKey - /// - [JsonProperty("defaultDisplayKey")] - public string DefaultDisplayKey { get; set; } - - } -} \ No newline at end of file + [JsonProperty("defaultDisplayKey")] + public string DefaultDisplayKey { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsNDisplayRoomPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsNDisplayRoomPropertiesConfig.cs index f65864b8..7b7d43b3 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsNDisplayRoomPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsNDisplayRoomPropertiesConfig.cs @@ -4,55 +4,55 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + +/// +/// +/// +public class EssentialsNDisplayRoomPropertiesConfig : EssentialsConferenceRoomPropertiesConfig +{ + + /// + /// Gets or sets the DefaultAudioBehavior + /// + [JsonProperty("defaultAudioBehavior")] + public string DefaultAudioBehavior { get; set; } + + /// + /// Gets or sets the DefaultVideoBehavior + /// + [JsonProperty("defaultVideoBehavior")] + public string DefaultVideoBehavior { get; set; } + + /// + /// Gets or sets the Displays + /// + [JsonProperty("displays")] + public Dictionary Displays { get; set; } + + /// + /// Constructor + /// + public EssentialsNDisplayRoomPropertiesConfig() + { + Displays = new Dictionary(); + } + +} + +/// +/// Represents a DisplayItem +/// +public class DisplayItem : IKeyName { /// - /// + /// Gets or sets the Key /// - public class EssentialsNDisplayRoomPropertiesConfig : EssentialsConferenceRoomPropertiesConfig - { - /// - /// Gets or sets the DefaultAudioBehavior - /// - [JsonProperty("defaultAudioBehavior")] - public string DefaultAudioBehavior { get; set; } - - /// - /// Gets or sets the DefaultVideoBehavior - /// - [JsonProperty("defaultVideoBehavior")] - public string DefaultVideoBehavior { get; set; } - - /// - /// Gets or sets the Displays - /// - [JsonProperty("displays")] - public Dictionary Displays { get; set; } - - /// - /// Constructor - /// - public EssentialsNDisplayRoomPropertiesConfig() - { - Displays = new Dictionary(); - } - - } + public string Key { get; set; } /// - /// Represents a DisplayItem + /// Gets or sets the Name /// - public class DisplayItem : IKeyName - { - /// - /// Gets or sets the Key - /// - public string Key { get; set; } - - /// - /// Gets or sets the Name - /// - public string Name { get; set; } - } + public string Name { get; set; } +} -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs index 7de2405a..9f563489 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs @@ -7,70 +7,71 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Privacy; using Serilog.Events; -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + +/// +/// Helper class for EssentialsRoomConfig +/// +public class EssentialsRoomConfigHelper { /// - /// Represents a EssentialsRoomConfigHelper + /// Gets and operating, standalone emergegncy object that can be plugged into a room. + /// Returns null if there is no emergency defined /// - public class EssentialsRoomConfigHelper + public static EssentialsRoomEmergencyBase GetEmergency(EssentialsRoomPropertiesConfig props, IEssentialsRoom room) { - /// - /// GetEmergency method - /// - public static EssentialsRoomEmergencyBase GetEmergency(EssentialsRoomPropertiesConfig props, IEssentialsRoom room) + // This emergency + var emergency = props.Emergency; + if (emergency != null) { - // This emergency - var emergency = props.Emergency; - if (emergency != null) - { - //switch on emergency type here. Right now only contact and shutdown - var e = new EssentialsRoomEmergencyContactClosure(room.Key + "-emergency", props.Emergency, room); - DeviceManager.AddDevice(e); - return e; - } + //switch on emergency type here. Right now only contact and shutdown + var e = new EssentialsRoomEmergencyContactClosure(room.Key + "-emergency", props.Emergency, room); + DeviceManager.AddDevice(e); + return e; + } + return null; + } + + /// + /// + /// + /// + /// + /// + /// + /// GetMicrophonePrivacy method + /// + public static MicrophonePrivacyController GetMicrophonePrivacy( + EssentialsRoomPropertiesConfig props, IPrivacy room) + { + var microphonePrivacy = props.MicrophonePrivacy; + if (microphonePrivacy == null) + { + Debug.LogMessage(LogEventLevel.Information, "Cannot create microphone privacy with null properties"); return null; } - - /// - /// - /// - /// - /// - /// - /// - /// GetMicrophonePrivacy method - /// - public static MicrophonePrivacyController GetMicrophonePrivacy( - EssentialsRoomPropertiesConfig props, IPrivacy room) + // Get the MicrophonePrivacy device from the device manager + var mP = (DeviceManager.GetDeviceForKey(props.MicrophonePrivacy.DeviceKey) as MicrophonePrivacyController); + // Set this room as the IPrivacy device + if (mP == null) { - var microphonePrivacy = props.MicrophonePrivacy; - if (microphonePrivacy == null) - { - Debug.LogMessage(LogEventLevel.Information, "Cannot create microphone privacy with null properties"); - return null; - } - // Get the MicrophonePrivacy device from the device manager - var mP = (DeviceManager.GetDeviceForKey(props.MicrophonePrivacy.DeviceKey) as MicrophonePrivacyController); - // Set this room as the IPrivacy device - if (mP == null) - { - Debug.LogMessage(LogEventLevel.Information, "ERROR: Selected device {0} is not MicrophonePrivacyController", props.MicrophonePrivacy.DeviceKey); - return null; - } - mP.SetPrivacyDevice(room); + Debug.LogMessage(LogEventLevel.Information, "ERROR: Selected device {0} is not MicrophonePrivacyController", props.MicrophonePrivacy.DeviceKey); + return null; + } + mP.SetPrivacyDevice(room); - var behaviour = props.MicrophonePrivacy.Behaviour.ToLower(); + var behaviour = props.MicrophonePrivacy.Behaviour.ToLower(); - if (behaviour == null) - { - Debug.LogMessage(LogEventLevel.Information, "WARNING: No behaviour defined for MicrophonePrivacyController"); - return null; - } - if (behaviour == "trackroomstate") - { - // Tie LED enable to room power state - var essRoom = room as IEssentialsRoom; - essRoom.OnFeedback.OutputChange += (o, a) => + if (behaviour == null) + { + Debug.LogMessage(LogEventLevel.Information, "WARNING: No behaviour defined for MicrophonePrivacyController"); + return null; + } + if (behaviour == "trackroomstate") + { + // Tie LED enable to room power state + var essRoom = room as IEssentialsRoom; + essRoom.OnFeedback.OutputChange += (o, a) => { if (essRoom.OnFeedback.BoolValue) mP.EnableLeds = true; @@ -78,13 +79,13 @@ namespace PepperDash.Essentials.Room.Config mP.EnableLeds = false; }; - mP.EnableLeds = essRoom.OnFeedback.BoolValue; - } - else if (behaviour == "trackcallstate") - { - // Tie LED enable to room power state - var inCallRoom = room as IHasInCallFeedback; - inCallRoom.InCallFeedback.OutputChange += (o, a) => + mP.EnableLeds = essRoom.OnFeedback.BoolValue; + } + else if (behaviour == "trackcallstate") + { + // Tie LED enable to room power state + var inCallRoom = room as IHasInCallFeedback; + inCallRoom.InCallFeedback.OutputChange += (o, a) => { if (inCallRoom.InCallFeedback.BoolValue) mP.EnableLeds = true; @@ -92,477 +93,335 @@ namespace PepperDash.Essentials.Room.Config mP.EnableLeds = false; }; - mP.EnableLeds = inCallRoom.InCallFeedback.BoolValue; - } - - return mP; + mP.EnableLeds = inCallRoom.InCallFeedback.BoolValue; } + return mP; } +} + +/// +/// +/// +public class EssentialsRoomPropertiesConfig +{ + [JsonProperty("addresses")] + public EssentialsRoomAddressPropertiesConfig Addresses { get; set; } + /// - /// Represents a EssentialsRoomPropertiesConfig + /// Gets or sets the Description /// - public class EssentialsRoomPropertiesConfig + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// Gets or sets the Emergency + /// + [JsonProperty("emergency")] + public EssentialsRoomEmergencyConfig Emergency { get; set; } + + /// + /// Gets or sets the Help + /// + [JsonProperty("help")] + public EssentialsHelpPropertiesConfig Help { get; set; } + + /// + /// Gets or sets the HelpMessage + /// + [JsonProperty("helpMessage")] + public string HelpMessage { get; set; } + + /// + /// Read this value to get the help message. It checks for the old and new config format. + /// + public string HelpMessageForDisplay { - /// - /// Gets or sets the Addresses - /// - [JsonProperty("addresses")] - public EssentialsRoomAddressPropertiesConfig Addresses { get; set; } - - /// - /// Gets or sets the Description - /// - [JsonProperty("description")] - public string Description { get; set; } - - /// - /// Gets or sets the Emergency - /// - [JsonProperty("emergency")] - public EssentialsRoomEmergencyConfig Emergency { get; set; } - - /// - /// Gets or sets the Help - /// - [JsonProperty("help")] - public EssentialsHelpPropertiesConfig Help { get; set; } - - /// - /// Gets or sets the HelpMessage - /// - [JsonProperty("helpMessage")] - public string HelpMessage { get; set; } - - /// - /// Read this value to get the help message. It checks for the old and new config format. - /// - public string HelpMessageForDisplay + get { - get + if (Help != null && !string.IsNullOrEmpty(Help.Message)) { - if (Help != null && !string.IsNullOrEmpty(Help.Message)) - { - return Help.Message; - } - else - { - return HelpMessage; - } + return Help.Message; } - } - - /// - /// Gets or sets the Environment - /// - [JsonProperty("environment")] - public EssentialsEnvironmentPropertiesConfig Environment { get; set; } - - /// - /// Gets or sets the LogoLight - /// - [JsonProperty("logo")] - public EssentialsLogoPropertiesConfig LogoLight { get; set; } - - /// - /// Gets or sets the LogoDark - /// - [JsonProperty("logoDark")] - public EssentialsLogoPropertiesConfig LogoDark { get; set; } - - /// - /// Gets or sets the MicrophonePrivacy - /// - [JsonProperty("microphonePrivacy")] - public EssentialsRoomMicrophonePrivacyConfig MicrophonePrivacy { get; set; } - - /// - /// Gets or sets the Occupancy - /// - [JsonProperty("occupancy")] - public EssentialsRoomOccSensorConfig Occupancy { get; set; } - - /// - /// Gets or sets the OneButtonMeeting - /// - [JsonProperty("oneButtonMeeting")] - public EssentialsOneButtonMeetingPropertiesConfig OneButtonMeeting { get; set; } - - /// - /// Gets or sets the ShutdownVacancySeconds - /// - [JsonProperty("shutdownVacancySeconds")] - public int ShutdownVacancySeconds { get; set; } - - /// - /// Gets or sets the ShutdownPromptSeconds - /// - [JsonProperty("shutdownPromptSeconds")] - public int ShutdownPromptSeconds { get; set; } - - /// - /// Gets or sets the Tech - /// - [JsonProperty("tech")] - public EssentialsRoomTechConfig Tech { get; set; } - - /// - /// Gets or sets the Volumes - /// - [JsonProperty("volumes")] - public EssentialsRoomVolumesConfig Volumes { get; set; } - - /// - /// Gets or sets the Fusion - /// - [JsonProperty("fusion")] - public EssentialsRoomFusionConfig Fusion { get; set; } - - /// - /// Gets or sets the UiBehavior - /// - [JsonProperty("essentialsRoomUiBehaviorConfig", NullValueHandling = NullValueHandling.Ignore)] - public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; } - - /// - /// Gets or sets the ZeroVolumeWhenSwtichingVolumeDevices - /// - [JsonProperty("zeroVolumeWhenSwtichingVolumeDevices")] - public bool ZeroVolumeWhenSwtichingVolumeDevices { get; set; } - - /// - /// Indicates if this room represents a combination of other rooms - /// - [JsonProperty("isRoomCombinationScenario")] - public bool IsRoomCombinationScenario { get; set; } - - /// - /// Constructor - /// - public EssentialsRoomPropertiesConfig() - { - LogoLight = new EssentialsLogoPropertiesConfig(); - LogoDark = new EssentialsLogoPropertiesConfig(); - } - } - - /// - /// Represents a EssentialsRoomUiBehaviorConfig - /// - public class EssentialsRoomUiBehaviorConfig - { - /// - /// Gets or sets the DisableActivityButtonsWhileWarmingCooling - /// - [JsonProperty("disableActivityButtonsWhileWarmingCooling")] - public bool DisableActivityButtonsWhileWarmingCooling { get; set; } - } - - /// - /// Represents a EssentialsAvRoomPropertiesConfig - /// - public class EssentialsAvRoomPropertiesConfig : EssentialsRoomPropertiesConfig - { - /// - /// Gets or sets the DefaultAudioKey - /// - [JsonProperty("defaultAudioKey")] - public string DefaultAudioKey { get; set; } - - /// - /// Gets or sets the DefaultOnDspPresetKey - /// - [JsonProperty("defaultOnDspPresetKey")] - public string DefaultOnDspPresetKey { get; set; } - - /// - /// Gets or sets the DefaultOffDspPresetKey - /// - [JsonProperty("defaultOffDspPresetKey")] - public string DefaultOffDspPresetKey { get; set; } - - /// - /// Gets or sets the SourceListKey - /// - [JsonProperty("sourceListKey")] - public string SourceListKey { get; set; } - /// - /// Gets or sets the DestinationListKey - /// - [JsonProperty("destinationListKey")] - public string DestinationListKey { get; set; } - /// - /// Gets or sets the AudioControlPointListKey - /// - [JsonProperty("audioControlPointListKey")] - public string AudioControlPointListKey { get; set; } - /// - /// Gets or sets the CameraListKey - /// - [JsonProperty("cameraListKey")] - public string CameraListKey { get; set; } - - - /// - /// Gets or sets the DefaultSourceItem - /// - [JsonProperty("defaultSourceItem")] - public string DefaultSourceItem { get; set; } - /// - /// Indicates if the room supports advanced sharing - /// - [JsonProperty("supportsAdvancedSharing")] - public bool SupportsAdvancedSharing { get; set; } - - /// - /// Indicates if non-tech users can change the share mode - /// - [JsonProperty("userCanChangeShareMode")] - public bool UserCanChangeShareMode { get; set; } - - - /// - /// Gets or sets the MatrixRoutingKey - /// - [JsonProperty("matrixRoutingKey", NullValueHandling = NullValueHandling.Ignore)] - public string MatrixRoutingKey { get; set; } - } - - /// - /// Represents a EssentialsConferenceRoomPropertiesConfig - /// - public class EssentialsConferenceRoomPropertiesConfig : EssentialsAvRoomPropertiesConfig - { - /// - /// Gets or sets the VideoCodecKey - /// - [JsonProperty("videoCodecKey")] - public string VideoCodecKey { get; set; } - /// - /// Gets or sets the AudioCodecKey - /// - [JsonProperty("audioCodecKey")] - public string AudioCodecKey { get; set; } - - } - - /// - /// Represents a EssentialsEnvironmentPropertiesConfig - /// - public class EssentialsEnvironmentPropertiesConfig - { - /// - /// Gets or sets the Enabled - /// - public bool Enabled { get; set; } - - /// - /// Gets or sets the DeviceKeys - /// - [JsonProperty("deviceKeys")] - public List DeviceKeys { get; set; } - - /// - /// Constructor - /// - public EssentialsEnvironmentPropertiesConfig() - { - DeviceKeys = new List(); - } - - } - - /// - /// Represents a EssentialsRoomFusionConfig - /// - public class EssentialsRoomFusionConfig - { - /// - /// Gets the the IpId as a UInt16 - /// - public uint IpIdInt - { - get + else { - try - { - return Convert.ToUInt32(IpId, 16); - } - catch (Exception) - { - throw new FormatException(string.Format("ERROR:Unable to convert IP ID: {0} to hex. Error:\n{1}", IpId)); - } - + return HelpMessage; } } - - /// - /// Gets or sets the IpId - /// - [JsonProperty("ipId")] - public string IpId { get; set; } - - /// - /// Gets or sets the JoinMapKey - /// - [JsonProperty("joinMapKey")] - public string JoinMapKey { get; set; } - } /// - /// Represents a EssentialsRoomMicrophonePrivacyConfig + /// Gets or sets the Environment /// - public class EssentialsRoomMicrophonePrivacyConfig - { - /// - /// Gets or sets the DeviceKey - /// - [JsonProperty("deviceKey")] - public string DeviceKey { get; set; } - - /// - /// Gets or sets the Behaviour - /// - [JsonProperty("behaviour")] - public string Behaviour { get; set; } - } + [JsonProperty("environment")] + public EssentialsEnvironmentPropertiesConfig Environment { get; set; } /// - /// Represents a EssentialsHelpPropertiesConfig + /// Gets or sets the LogoLight /// - public class EssentialsHelpPropertiesConfig + [JsonProperty("logo")] + public EssentialsLogoPropertiesConfig LogoLight { get; set; } + + /// + /// Gets or sets the LogoDark + /// + [JsonProperty("logoDark")] + public EssentialsLogoPropertiesConfig LogoDark { get; set; } + + /// + /// Gets or sets the MicrophonePrivacy + /// + [JsonProperty("microphonePrivacy")] + public EssentialsRoomMicrophonePrivacyConfig MicrophonePrivacy { get; set; } + + /// + /// Gets or sets the Occupancy + /// + [JsonProperty("occupancy")] + public EssentialsRoomOccSensorConfig Occupancy { get; set; } + + /// + /// Gets or sets the OneButtonMeeting + /// + [JsonProperty("oneButtonMeeting")] + public EssentialsOneButtonMeetingPropertiesConfig OneButtonMeeting { get; set; } + + /// + /// Gets or sets the ShutdownVacancySeconds + /// + [JsonProperty("shutdownVacancySeconds")] + public int ShutdownVacancySeconds { get; set; } + + /// + /// Gets or sets the ShutdownPromptSeconds + /// + [JsonProperty("shutdownPromptSeconds")] + public int ShutdownPromptSeconds { get; set; } + + /// + /// Gets or sets the Tech + /// + [JsonProperty("tech")] + public EssentialsRoomTechConfig Tech { get; set; } + + [JsonProperty("fusion")] + public EssentialsRoomFusionConfig Fusion { get; set; } + + [JsonProperty("essentialsRoomUiBehaviorConfig", NullValueHandling = NullValueHandling.Ignore)] + public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; } + + /// + /// Gets or sets the ZeroVolumeWhenSwtichingVolumeDevices + /// + [JsonProperty("zeroVolumeWhenSwtichingVolumeDevices")] + public bool ZeroVolumeWhenSwtichingVolumeDevices { get; set; } + + /// + /// Indicates if this room represents a combination of other rooms + /// + [JsonProperty("isRoomCombinationScenario")] + public bool IsRoomCombinationScenario { get; set; } + + public EssentialsRoomPropertiesConfig() { - /// - /// Gets or sets the Message - /// - [JsonProperty("message")] - public string Message { get; set; } + LogoLight = new EssentialsLogoPropertiesConfig(); + LogoDark = new EssentialsLogoPropertiesConfig(); + } +} - /// - /// Gets or sets the ShowCallButton - /// - [JsonProperty("showCallButton")] - public bool ShowCallButton { get; set; } +public class EssentialsRoomUiBehaviorConfig +{ + [JsonProperty("disableActivityButtonsWhileWarmingCooling")] + public bool DisableActivityButtonsWhileWarmingCooling { get; set; } +} - /// - /// Defaults to "Call Help Desk" - /// - [JsonProperty("callButtonText")] - public string CallButtonText { get; set; } +public class EssentialsAvRoomPropertiesConfig : EssentialsRoomPropertiesConfig +{ + [JsonProperty("defaultAudioKey")] + public string DefaultAudioKey { get; set; } + [JsonProperty("sourceListKey")] + public string SourceListKey { get; set; } + [JsonProperty("destinationListKey")] + public string DestinationListKey { get; set; } + [JsonProperty("audioControlPointListKey")] + public string AudioControlPointListKey { get; set; } + [JsonProperty("cameraListKey")] + public string CameraListKey { get; set; } - /// - /// Constructor - /// - public EssentialsHelpPropertiesConfig() + + [JsonProperty("defaultSourceItem")] + public string DefaultSourceItem { get; set; } + /// + /// Indicates if the room supports advanced sharing + /// + [JsonProperty("supportsAdvancedSharing")] + public bool SupportsAdvancedSharing { get; set; } + /// + /// Indicates if non-tech users can change the share mode + /// + [JsonProperty("userCanChangeShareMode")] + public bool UserCanChangeShareMode { get; set; } + + + [JsonProperty("matrixRoutingKey", NullValueHandling = NullValueHandling.Ignore)] + public string MatrixRoutingKey { get; set; } +} + +public class EssentialsConferenceRoomPropertiesConfig : EssentialsAvRoomPropertiesConfig +{ + [JsonProperty("videoCodecKey")] + public string VideoCodecKey { get; set; } + [JsonProperty("audioCodecKey")] + public string AudioCodecKey { get; set; } + +} + +/// +/// Represents a EssentialsEnvironmentPropertiesConfig +/// +public class EssentialsEnvironmentPropertiesConfig +{ + /// + /// Gets or sets the Enabled + /// + public bool Enabled { get; set; } + + [JsonProperty("deviceKeys")] + public List DeviceKeys { get; set; } + + public EssentialsEnvironmentPropertiesConfig() + { + DeviceKeys = new List(); + } + +} + +public class EssentialsRoomFusionConfig +{ + public uint IpIdInt + { + get { - CallButtonText = "Call Help Desk"; + try + { + return Convert.ToUInt32(IpId, 16); + } + catch (Exception) + { + throw new FormatException(string.Format("ERROR:Unable to convert IP ID: {0} to hex. Error:\n{1}", IpId)); + } + } } - /// - /// Represents a EssentialsOneButtonMeetingPropertiesConfig - /// - public class EssentialsOneButtonMeetingPropertiesConfig - { - /// - /// Gets or sets the Enable - /// - [JsonProperty("enable")] - public bool Enable { get; set; } - } + [JsonProperty("ipId")] + public string IpId { get; set; } + + [JsonProperty("joinMapKey")] + public string JoinMapKey { get; set; } + +} + +public class EssentialsRoomMicrophonePrivacyConfig +{ + [JsonProperty("deviceKey")] + public string DeviceKey { get; set; } + + [JsonProperty("behaviour")] + public string Behaviour { get; set; } +} + +/// +/// Properties for the help text box +/// +public class EssentialsHelpPropertiesConfig +{ + [JsonProperty("message")] + public string Message { get; set; } + + [JsonProperty("showCallButton")] + public bool ShowCallButton { get; set; } /// - /// Represents a EssentialsRoomAddressPropertiesConfig + /// Defaults to "Call Help Desk" /// - public class EssentialsRoomAddressPropertiesConfig + [JsonProperty("callButtonText")] + public string CallButtonText { get; set; } + + public EssentialsHelpPropertiesConfig() { - /// - /// Gets or sets the PhoneNumber - /// - [JsonProperty("phoneNumber")] - public string PhoneNumber { get; set; } - - /// - /// Gets or sets the SipAddress - /// - [JsonProperty("sipAddress")] - public string SipAddress { get; set; } + CallButtonText = "Call Help Desk"; } +} + +/// +/// +/// +public class EssentialsOneButtonMeetingPropertiesConfig +{ + [JsonProperty("enable")] + public bool Enable { get; set; } +} + +public class EssentialsRoomAddressPropertiesConfig +{ + [JsonProperty("phoneNumber")] + public string PhoneNumber { get; set; } + + [JsonProperty("sipAddress")] + public string SipAddress { get; set; } +} +/// +/// Properties for the room's logo on panels +/// +public class EssentialsLogoPropertiesConfig +{ + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } /// - /// Represents a EssentialsLogoPropertiesConfig + /// Gets either the custom URL, a local-to-processor URL, or null if it's a default logo /// - public class EssentialsLogoPropertiesConfig + public string GetLogoUrlLight() { - /// - /// Gets or sets the Type - /// - [JsonProperty("type")] - public string Type { get; set; } - - /// - /// Gets or sets the Url - /// - [JsonProperty("url")] - public string Url { get; set; } - - /// - /// GetLogoUrlLight method - /// - public string GetLogoUrlLight() - { - if (Type == "url") - return Url; - if (Type == "system") - return string.Format("http://{0}:8080/logo.png", - CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)); - return null; - } - - /// - /// GetLogoUrlDark method - /// - public string GetLogoUrlDark() - { - if (Type == "url") - return Url; - if (Type == "system") - return string.Format("http://{0}:8080/logo-dark.png", - CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)); - return null; - } + if (Type == "url") + return Url; + if (Type == "system") + return string.Format("http://{0}:8080/logo.png", + CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)); + return null; } - /// - /// Represents a EssentialsRoomOccSensorConfig - /// - public class EssentialsRoomOccSensorConfig + public string GetLogoUrlDark() { - /// - /// Gets or sets the DeviceKey - /// - [JsonProperty("deviceKey")] - public string DeviceKey { get; set; } - - /// - /// Gets or sets the TimeoutMinutes - /// - [JsonProperty("timeoutMinutes")] - public int TimeoutMinutes { get; set; } + if (Type == "url") + return Url; + if (Type == "system") + return string.Format("http://{0}:8080/logo-dark.png", + CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)); + return null; } +} - /// - /// Represents a EssentialsRoomTechConfig - /// - public class EssentialsRoomTechConfig - { - /// - /// Gets or sets the Password - /// - [JsonProperty("password")] - public string Password { get; set; } - } -} \ No newline at end of file +/// +/// Represents occupancy sensor(s) setup for a room +/// +public class EssentialsRoomOccSensorConfig +{ + [JsonProperty("deviceKey")] + public string DeviceKey { get; set; } + + [JsonProperty("timeoutMinutes")] + public int TimeoutMinutes { get; set; } +} + +public class EssentialsRoomTechConfig +{ + [JsonProperty("password")] + public string Password { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomEmergencyConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomEmergencyConfig.cs index 349f44b8..f3440043 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomEmergencyConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomEmergencyConfig.cs @@ -1,40 +1,29 @@ -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + +/// +/// +/// +public class EssentialsRoomEmergencyConfig +{ + public EssentialsRoomEmergencyTriggerConfig Trigger { get; set; } + + public string Behavior { get; set; } +} + +/// +/// +/// +public class EssentialsRoomEmergencyTriggerConfig { /// - /// Represents a EssentialsRoomEmergencyConfig + /// contact,versiport /// - public class EssentialsRoomEmergencyConfig - { - /// - /// Gets or sets the Trigger - /// - public EssentialsRoomEmergencyTriggerConfig Trigger { get; set; } - - /// - /// Gets or sets the Behavior - /// - public string Behavior { get; set; } - } - + public string Type { get; set; } /// - /// Represents a EssentialsRoomEmergencyTriggerConfig + /// Input number if contact /// - public class EssentialsRoomEmergencyTriggerConfig - { - /// - /// contact,versiport - /// - public string Type { get; set; } + public int Number { get; set; } - /// - /// Input number if contact - /// - public int Number { get; set; } + public bool TriggerOnClose { get; set; } - /// - /// TriggerOnClose indicates if the trigger is on close - /// - public bool TriggerOnClose { get; set; } - - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomScheduledEventsConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomScheduledEventsConfig.cs index d7cb0514..b15bb9f8 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomScheduledEventsConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomScheduledEventsConfig.cs @@ -6,71 +6,71 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + + +/// +/// Represents a EssentialsRoomScheduledEventsConfig +/// +public class EssentialsRoomScheduledEventsConfig { /// - /// Represents a EssentialsRoomScheduledEventsConfig + /// Gets or sets the ScheduledEvents /// - public class EssentialsRoomScheduledEventsConfig - { - /// - /// Gets or sets the ScheduledEvents - /// - [JsonProperty("scheduledEvents")] - public List ScheduledEvents; - } + [JsonProperty("scheduledEvents")] + public List ScheduledEvents; +} + +/// +/// Represents a ScheduledEventConfig +/// +public class ScheduledEventConfig +{ + /// + /// Gets or sets the Key + /// + [JsonProperty("key")] + public string Key; /// - /// Represents a ScheduledEventConfig + /// Gets or sets the Name /// - public class ScheduledEventConfig - { - /// - /// Gets or sets the Key - /// - [JsonProperty("key")] - public string Key; + [JsonProperty("name")] + public string Name; - /// - /// Gets or sets the Name - /// - [JsonProperty("name")] - public string Name; + /// + /// Gets or sets the Days + /// + [JsonProperty("days")] + public ScheduledEventCommon.eWeekDays Days; - /// - /// Gets or sets the Days - /// - [JsonProperty("days")] - public ScheduledEventCommon.eWeekDays Days; + /// + /// Gets or sets the Time + /// + [JsonProperty("time")] + public string Time; - /// - /// Gets or sets the Time - /// - [JsonProperty("time")] - public string Time; + /// + /// Gets or sets the Actions + /// + [JsonProperty("actions")] + public List Actions; - /// - /// Gets or sets the Actions - /// - [JsonProperty("actions")] - public List Actions; + /// + /// Gets or sets the Persistent + /// + [JsonProperty("persistent")] + public bool Persistent; - /// - /// Gets or sets the Persistent - /// - [JsonProperty("persistent")] - public bool Persistent; + /// + /// Gets or sets the Acknowledgeable + /// + [JsonProperty("acknowledgeable")] + public bool Acknowledgeable; - /// - /// Gets or sets the Acknowledgeable - /// - [JsonProperty("acknowledgeable")] - public bool Acknowledgeable; - - /// - /// Gets or sets the Enable - /// - [JsonProperty("enable")] - public bool Enable; - } -} \ No newline at end of file + /// + /// Gets or sets the Enable + /// + [JsonProperty("enable")] + public bool Enable; +} diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsTechRoomConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsTechRoomConfig.cs index dfc3d7a6..086bf54f 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsTechRoomConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsTechRoomConfig.cs @@ -1,89 +1,87 @@  using System.Collections.Generic; using Newtonsoft.Json; -using PepperDash.Essentials.Room.Config; -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + +/// +/// Configuration class for the EssentialsTechRoom. This is used to deserialize the room configuration from JSON and provide it to the room on initialization. +/// +public class EssentialsTechRoomConfig { /// - /// Represents a EssentialsTechRoomConfig + /// The key of the dummy device used to enable routing /// - public class EssentialsTechRoomConfig + [JsonProperty("dummySourceKey")] + public string DummySourceKey { get; set; } + + /// + /// The keys of the displays assigned to this room + /// + [JsonProperty("displays")] + public List Displays { get; set; } + + /// + /// The keys of the tuners assigned to this room + /// + [JsonProperty("tuners")] + public List Tuners { get; set; } + + /// + /// PIN to access the room as a normal user + /// + [JsonProperty("userPin")] + public string UserPin { get; set; } + + /// + /// PIN to access the room as a tech user + /// + [JsonProperty("techPin")] + public string TechPin { get; set; } + + /// + /// Name of the presets file. Path prefix is assumed to be /html/presets/lists/ + /// + [JsonProperty("presetsFileName")] + public string PresetsFileName { get; set; } + + /// + /// List of scheduled events for the room + /// + [JsonProperty("scheduledEvents")] + public List ScheduledEvents { get; set; } + + /// + /// Indicates that the room is the primary when true + /// + [JsonProperty("isPrimary")] + public bool IsPrimary { get; set; } + + /// + /// Indicates which tuners should mirror preset recall when two rooms are configured in a primary->secondary scenario + /// + [JsonProperty("mirroredTuners")] + public Dictionary MirroredTuners { get; set; } + + /// + /// Help message to show on the UI when the user clicks the help button. Can be used to provide room specific instructions for users. + /// + [JsonProperty("helpMessage")] + public string HelpMessage { get; set; } + + /// + /// Indicates the room + /// + [JsonProperty("isTvPresetsProvider")] + public bool IsTvPresetsProvider; + + /// + /// Constructor + /// + public EssentialsTechRoomConfig() { - /// - /// The key of the dummy device used to enable routing - /// - [JsonProperty("dummySourceKey")] - public string DummySourceKey { get; set; } - - /// - /// The keys of the displays assigned to this room - /// - [JsonProperty("displays")] - public List Displays { get; set; } - - /// - /// The keys of the tuners assigned to this room - /// - [JsonProperty("tuners")] - public List Tuners { get; set; } - - /// - /// PIN to access the room as a normal user - /// - [JsonProperty("userPin")] - public string UserPin { get; set; } - - /// - /// PIN to access the room as a tech user - /// - [JsonProperty("techPin")] - public string TechPin { get; set; } - - /// - /// Name of the presets file. Path prefix is assumed to be /html/presets/lists/ - /// - [JsonProperty("presetsFileName")] - public string PresetsFileName { get; set; } - - /// - /// Gets or sets the ScheduledEvents - /// - [JsonProperty("scheduledEvents")] - public List ScheduledEvents { get; set; } - - /// - /// Indicates that the room is the primary when true - /// - [JsonProperty("isPrimary")] - public bool IsPrimary { get; set; } - - /// - /// Indicates which tuners should mirror preset recall when two rooms are configured in a primary->secondary scenario - /// - [JsonProperty("mirroredTuners")] - public Dictionary MirroredTuners { get; set; } - - /// - /// Gets or sets the HelpMessage - /// - [JsonProperty("helpMessage")] - public string HelpMessage { get; set; } - - /// - /// Gets or sets the IsTvPresetsProvider - /// - [JsonProperty("isTvPresetsProvider")] - public bool IsTvPresetsProvider; - - /// - /// Constructor - /// - public EssentialsTechRoomConfig() - { - Displays = new List(); - Tuners = new List(); - ScheduledEvents = new List(); - } + Displays = new List(); + Tuners = new List(); + ScheduledEvents = new List(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsVolumeLevelConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsVolumeLevelConfig.cs index 51172130..c1c34b9c 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsVolumeLevelConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsVolumeLevelConfig.cs @@ -1,115 +1,35 @@ using System; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + +/// +/// Configuration class for volume levels in the Essentials room. This is used to configure the volume levels for the master, program, audio call receive, and audio call transmit channels in the room. +/// +[Obsolete("This class is being deprecated in favor audio control point lists in the main config. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type.")] +public class EssentialsRoomVolumesConfig { - /// - /// Represents a EssentialsRoomVolumesConfig - /// - public class EssentialsRoomVolumesConfig - { - /// - /// Gets or sets the Master - /// - public EssentialsVolumeLevelConfig Master { get; set; } + public EssentialsVolumeLevelConfig Master { get; set; } + public EssentialsVolumeLevelConfig Program { get; set; } + public EssentialsVolumeLevelConfig AudioCallRx { get; set; } + public EssentialsVolumeLevelConfig AudioCallTx { get; set; } +} - /// - /// Gets or sets the Program - /// - public EssentialsVolumeLevelConfig Program { get; set; } - - /// - /// Gets or sets the AudioCallRx - /// - public EssentialsVolumeLevelConfig AudioCallRx { get; set; } - - /// - /// Gets or sets the AudioCallTx - /// - public EssentialsVolumeLevelConfig AudioCallTx { get; set; } - } +/// +/// Configuration class for a volume level in the Essentials room. +/// +public class EssentialsVolumeLevelConfig +{ + public string DeviceKey { get; set; } + public string Label { get; set; } + public int Level { get; set; } /// - /// Represents a EssentialsVolumeLevelConfig + /// Helper to get the device associated with key - one timer. /// - public class EssentialsVolumeLevelConfig + [Obsolete("This method references DM CHASSIS Directly and should not be used in the Core library. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type.")] + public IBasicVolumeWithFeedback GetDevice() { - /// - /// Gets or sets the DeviceKey - /// - public string DeviceKey { get; set; } - /// - /// Gets or sets the Label - /// - public string Label { get; set; } - /// - /// Gets or sets the Level - /// - public int Level { get; set; } - - /// - /// Helper to get the device associated with key - one timer. - /// - public IBasicVolumeWithFeedback GetDevice() - { - throw new NotImplementedException("This method references DM CHASSIS Directly"); - /* - // DM output card format: deviceKey--output~number, dm8x8-1--output~4 - var match = Regex.Match(DeviceKey, @"([-_\w]+)--(\w+)~(\d+)"); - if (match.Success) - { - var devKey = match.Groups[1].Value; - var chassis = DeviceManager.GetDeviceForKey(devKey) as DmChassisController; - if (chassis != null) - { - var outputNum = Convert.ToUInt32(match.Groups[3].Value); - if (chassis.VolumeControls.ContainsKey(outputNum)) // should always... - return chassis.VolumeControls[outputNum]; - } - // No volume for some reason. We have failed as developers - return null; - } - - // DSP/DMPS format: deviceKey--levelName, biampTesira-1--master - match = Regex.Match(DeviceKey, @"([-_\w]+)--(.+)"); - if (match.Success) - { - var devKey = match.Groups[1].Value; - var dsp = DeviceManager.GetDeviceForKey(devKey) as BiampTesiraForteDsp; - if (dsp != null) - { - var levelTag = match.Groups[2].Value; - if (dsp.LevelControlPoints.ContainsKey(levelTag)) // should always... - return dsp.LevelControlPoints[levelTag]; - } - - var dmps = DeviceManager.GetDeviceForKey(devKey) as DmpsAudioOutputController; - if (dmps != null) - { - var levelTag = match.Groups[2].Value; - switch (levelTag) - { - case "master": - return dmps.MasterVolumeLevel; - case "source": - return dmps.SourceVolumeLevel; - case "micsmaster": - return dmps.MicsMasterVolumeLevel; - case "codec1": - return dmps.Codec1VolumeLevel; - case "codec2": - return dmps.Codec2VolumeLevel; - default: - return dmps.MasterVolumeLevel; - } - } - // No volume for some reason. We have failed as developers - return null; - } - - return null; - } - * */ - } + throw new NotImplementedException("This method references DM CHASSIS Directly and should not be used in the Core library. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type."); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Config/SimplRoomPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/SimplRoomPropertiesConfig.cs index 59628aed..dad8b9f2 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/SimplRoomPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/SimplRoomPropertiesConfig.cs @@ -1,53 +1,52 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace PepperDash.Essentials.Room.Config +namespace PepperDash.Essentials.Room.Config; + +/// +/// Represents a SimplRoomPropertiesConfig +/// +public class SimplRoomPropertiesConfig : EssentialsHuddleVtc1PropertiesConfig { - /// - /// Represents a SimplRoomPropertiesConfig - /// - public class SimplRoomPropertiesConfig : EssentialsHuddleVtc1PropertiesConfig - { - /// - /// Gets or sets the RoomPhoneNumber - /// - [JsonProperty("roomPhoneNumber")] - public string RoomPhoneNumber { get; set; } - - /// - /// Gets or sets the RoomURI - /// - [JsonProperty("roomURI")] - public string RoomURI { get; set; } - - /// - /// Gets or sets the SpeedDials - /// - [JsonProperty("speedDials")] - public List SpeedDials { get; set; } - - /// - /// Gets or sets the VolumeSliderNames - /// - [JsonProperty("volumeSliderNames")] - public List VolumeSliderNames { get; set; } - } + /// + /// Gets or sets the RoomPhoneNumber + /// + [JsonProperty("roomPhoneNumber")] + public string RoomPhoneNumber { get; set; } /// - /// Represents a SimplSpeedDial + /// Gets or sets the RoomURI /// - public class SimplSpeedDial - { - /// - /// Gets or sets the Name - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// Gets or sets the Number - /// - [JsonProperty("number")] - public string Number { get; set; } - } + [JsonProperty("roomURI")] + public string RoomURI { get; set; } + + /// + /// Gets or sets the SpeedDials + /// + [JsonProperty("speedDials")] + public List SpeedDials { get; set; } + + /// + /// Gets or sets the VolumeSliderNames + /// + [JsonProperty("volumeSliderNames")] + public List VolumeSliderNames { get; set; } +} + +/// +/// Represents a SimplSpeedDial +/// +public class SimplSpeedDial +{ + /// + /// Gets or sets the Name + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the Number + /// + [JsonProperty("number")] + public string Number { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/EsentialsRoomEmergencyContactClosure.cs b/src/PepperDash.Essentials.Core/Room/EsentialsRoomEmergencyContactClosure.cs index 6582bac3..ee81c4d0 100644 --- a/src/PepperDash.Essentials.Core/Room/EsentialsRoomEmergencyContactClosure.cs +++ b/src/PepperDash.Essentials.Core/Room/EsentialsRoomEmergencyContactClosure.cs @@ -2,116 +2,94 @@ using Crestron.SimplSharpPro; using PepperDash.Essentials.Room.Config; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public class EssentialsRoomEmergencyContactClosure : EssentialsRoomEmergencyBase, IEssentialsRoomEmergency { - /// - /// Represents a EssentialsRoomEmergencyContactClosure - /// - public class EssentialsRoomEmergencyContactClosure : EssentialsRoomEmergencyBase, IEssentialsRoomEmergency + public event EventHandler EmergencyStateChange; + + IEssentialsRoom Room; + string Behavior; + bool TriggerOnClose; + + public bool InEmergency { get; private set; } + + public EssentialsRoomEmergencyContactClosure(string key, EssentialsRoomEmergencyConfig config, IEssentialsRoom room) : + base(key) { - /// - /// Event fired when emergency state changes - /// - public event EventHandler EmergencyStateChange; + Room = room; + var cs = Global.ControlSystem; - IEssentialsRoom Room; - string Behavior; - bool TriggerOnClose; - - /// - /// Gets or sets the InEmergency - /// - public bool InEmergency { get; private set; } - - /// - /// Constructor for EssentialsRoomEmergencyContactClosure - /// - /// device key - /// emergency device config - /// the room associated with this emergency contact closure - public EssentialsRoomEmergencyContactClosure(string key, EssentialsRoomEmergencyConfig config, IEssentialsRoom room) : - base(key) + if (config.Trigger.Type.Equals("contact", StringComparison.OrdinalIgnoreCase)) { - Room = room; - var cs = Global.ControlSystem; - - if (config.Trigger.Type.Equals("contact", StringComparison.OrdinalIgnoreCase)) + var portNum = (uint)config.Trigger.Number; + if (portNum <= cs.NumberOfDigitalInputPorts) { - var portNum = (uint)config.Trigger.Number; - if (portNum <= cs.NumberOfDigitalInputPorts) - { - cs.DigitalInputPorts[portNum].Register(); - cs.DigitalInputPorts[portNum].StateChange += EsentialsRoomEmergencyContactClosure_StateChange; - } - } - else if (config.Trigger.Type.Equals("versiport", StringComparison.OrdinalIgnoreCase)) - { - var portNum = (uint)config.Trigger.Number; - if (portNum <= cs.NumberOfVersiPorts) - { - cs.VersiPorts[portNum].Register(); - cs.VersiPorts[portNum].SetVersiportConfiguration(eVersiportConfiguration.DigitalInput); - cs.VersiPorts[portNum].DisablePullUpResistor = true; - cs.VersiPorts[portNum].VersiportChange += EssentialsRoomEmergencyContactClosure_VersiportChange; - } - } - Behavior = config.Behavior; - TriggerOnClose = config.Trigger.TriggerOnClose; - } - - private void EssentialsRoomEmergencyContactClosure_VersiportChange(Versiport port, VersiportEventArgs args) - { - if (args.Event == eVersiportEvent.DigitalInChange) - { - ContactClosure_StateChange(port.DigitalIn); + cs.DigitalInputPorts[portNum].Register(); + cs.DigitalInputPorts[portNum].StateChange += EsentialsRoomEmergencyContactClosure_StateChange; } } - - void EsentialsRoomEmergencyContactClosure_StateChange(DigitalInput digitalInput, DigitalInputEventArgs args) + else if (config.Trigger.Type.Equals("versiport", StringComparison.OrdinalIgnoreCase)) { - ContactClosure_StateChange(args.State); - } - - void ContactClosure_StateChange(bool portState) - { - if (portState && TriggerOnClose || !portState && !TriggerOnClose) + var portNum = (uint)config.Trigger.Number; + if (portNum <= cs.NumberOfVersiPorts) { - InEmergency = true; - if (EmergencyStateChange != null) - EmergencyStateChange(this, new EventArgs()); - RunEmergencyBehavior(); - } - else - { - InEmergency = false; - if (EmergencyStateChange != null) - EmergencyStateChange(this, new EventArgs()); + cs.VersiPorts[portNum].Register(); + cs.VersiPorts[portNum].SetVersiportConfiguration(eVersiportConfiguration.DigitalInput); + cs.VersiPorts[portNum].DisablePullUpResistor = true; + cs.VersiPorts[portNum].VersiportChange += EssentialsRoomEmergencyContactClosure_VersiportChange; } } + Behavior = config.Behavior; + TriggerOnClose = config.Trigger.TriggerOnClose; + } - /// - /// RunEmergencyBehavior method - /// - public void RunEmergencyBehavior() + private void EssentialsRoomEmergencyContactClosure_VersiportChange(Versiport port, VersiportEventArgs args) + { + if (args.Event == eVersiportEvent.DigitalInChange) { - if (Behavior.Equals("shutdown")) - Room.Shutdown(); + ContactClosure_StateChange(port.DigitalIn); + } + } + + void EsentialsRoomEmergencyContactClosure_StateChange(DigitalInput digitalInput, DigitalInputEventArgs args) + { + ContactClosure_StateChange(args.State); + } + + void ContactClosure_StateChange(bool portState) + { + if (portState && TriggerOnClose || !portState && !TriggerOnClose) + { + InEmergency = true; + if (EmergencyStateChange != null) + EmergencyStateChange(this, new EventArgs()); + RunEmergencyBehavior(); + } + else + { + InEmergency = false; + if (EmergencyStateChange != null) + EmergencyStateChange(this, new EventArgs()); } } /// - /// Defines the contract for IEssentialsRoomEmergency + /// /// - public interface IEssentialsRoomEmergency + public void RunEmergencyBehavior() { - /// - /// Event fired when emergency state changes - /// - event EventHandler EmergencyStateChange; - - /// - /// Gets or sets the InEmergency - /// - bool InEmergency { get; } + if (Behavior.Equals("shutdown")) + Room.Shutdown(); } +} + +/// +/// Describes the functionality of a room emergency contact closure +/// +public interface IEssentialsRoomEmergency +{ + event EventHandler EmergencyStateChange; + + bool InEmergency { get; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs b/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs index 13ba9d66..e2d35c2a 100644 --- a/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs +++ b/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs @@ -12,259 +12,188 @@ using PepperDash.Essentials.Core.Devices; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// +/// +public abstract class EssentialsRoomBase : ReconfigurableDevice, IEssentialsRoom { /// - /// + /// /// - public abstract class EssentialsRoomBase : ReconfigurableDevice, IEssentialsRoom + public BoolFeedback OnFeedback { get; private set; } + + /// + /// Fires when the RoomOccupancy object is set + /// + public event EventHandler RoomOccupancyIsSet; + + public BoolFeedback IsWarmingUpFeedback { get; private set; } + public BoolFeedback IsCoolingDownFeedback { get; private set; } + + public IOccupancyStatusProvider RoomOccupancy { get; protected set; } + + public bool OccupancyStatusProviderIsRemote { get; private set; } + + public List EnvironmentalControlDevices { get; protected set; } + + public bool HasEnvironmentalControlDevices { - /// - /// - /// - public BoolFeedback OnFeedback { get; private set; } - - /// - /// Fires when the RoomOccupancy object is set - /// - public event EventHandler RoomOccupancyIsSet; - - /// - /// Gets or sets the IsWarmingUpFeedback - /// - public BoolFeedback IsWarmingUpFeedback { get; private set; } - - /// - /// Gets or sets the IsCoolingDownFeedback - /// - public BoolFeedback IsCoolingDownFeedback { get; private set; } - - /// - /// Gets or sets the RoomOccupancy - /// - public IOccupancyStatusProvider RoomOccupancy { get; protected set; } - - /// - /// Gets or sets the OccupancyStatusProviderIsRemote - /// - public bool OccupancyStatusProviderIsRemote { get; private set; } - - /// - /// Gets or sets the EnvironmentalControlDevices - /// - public List EnvironmentalControlDevices { get; protected set; } - - /// - /// Indicates if the room has any environmental control devices - /// - public bool HasEnvironmentalControlDevices + get { - get - { - return EnvironmentalControlDevices != null && EnvironmentalControlDevices.Count > 0; - } + return EnvironmentalControlDevices != null && EnvironmentalControlDevices.Count > 0; } + } - /// - /// Gets or sets the IsWarmingFeedbackFunc - /// - protected abstract Func IsWarmingFeedbackFunc { get; } + protected abstract Func IsWarmingFeedbackFunc { get; } + protected abstract Func IsCoolingFeedbackFunc { get; } - /// - /// Gets or sets the IsCoolingFeedbackFunc - /// - protected abstract Func IsCoolingFeedbackFunc { get; } + /// + /// Indicates if this room is Mobile Control Enabled + /// + public bool IsMobileControlEnabled { get; private set; } - /// - /// Gets or sets the IsMobileControlEnabled - /// - public bool IsMobileControlEnabled { get; private set; } + /// + /// The bridge for this room if Mobile Control is enabled + /// + public IMobileControlRoomMessenger MobileControlRoomBridge { get; private set; } - /// - /// Gets or sets the MobileControlRoomBridge - /// - public IMobileControlRoomMessenger MobileControlRoomBridge { get; private set; } + protected const string _defaultListKey = "default"; - /// - /// The config name of the default source list - /// - protected const string _defaultListKey = "default"; - - /// - /// The config name of the source list - /// + /// + /// The config name of the source list + /// /// private string _sourceListKey; - - /// - /// Gets or sets the SourceListKey - /// - public string SourceListKey { + public string SourceListKey { get { - if(string.IsNullOrEmpty(_sourceListKey)) - { - return _defaultListKey; - } - else - { - return _sourceListKey; - } + if(string.IsNullOrEmpty(_sourceListKey)) + { + return _defaultListKey; + } + else + { + return _sourceListKey; + } } protected set { - if (value != _sourceListKey) - { - _sourceListKey = value; - } + if (value != _sourceListKey) + { + _sourceListKey = value; + } } } - private string _destinationListKey; - - /// - /// Gets or sets the DestinationListKey - /// - public string DestinationListKey + private string _destinationListKey; + public string DestinationListKey + { + get { - get + if (string.IsNullOrEmpty(_destinationListKey)) { - if (string.IsNullOrEmpty(_destinationListKey)) - { - return _defaultListKey; - } - else - { - return _destinationListKey; - } + return _defaultListKey; } - protected set + else { - if (value != _destinationListKey) - { - _destinationListKey = value; - } + return _destinationListKey; } } - - private string _audioControlPointListKey; - - /// - /// Gets or sets the AudioControlPointListKey - /// - public string AudioControlPointListKey + protected set { - get + if (value != _destinationListKey) { - if (string.IsNullOrEmpty(_audioControlPointListKey)) - { - return _defaultListKey; - } - else - { - return _destinationListKey; - } - } - protected set - { - if (value != _audioControlPointListKey) - { - _audioControlPointListKey = value; - } + _destinationListKey = value; } } + } - private string _cameraListKey; - - /// - /// Gets or sets the CameraListKey - /// - public string CameraListKey + private string _audioControlPointListKey; + public string AudioControlPointListKey + { + get { - get + if (string.IsNullOrEmpty(_audioControlPointListKey)) { - if (string.IsNullOrEmpty(_cameraListKey)) - { - return _defaultListKey; - } - else - { - return _cameraListKey; - } + return _defaultListKey; } - protected set + else { - if (value != _cameraListKey) - { - _cameraListKey = value; - } + return _destinationListKey; } } + protected set + { + if (value != _audioControlPointListKey) + { + _audioControlPointListKey = value; + } + } + } - /// - /// Gets or sets the ShutdownPromptTimer - /// - public SecondsCountdownTimer ShutdownPromptTimer { get; private set; } + private string _cameraListKey; + public string CameraListKey + { + get + { + if (string.IsNullOrEmpty(_cameraListKey)) + { + return _defaultListKey; + } + else + { + return _cameraListKey; + } + } + protected set + { + if (value != _cameraListKey) + { + _cameraListKey = value; + } + } + } - /// - /// Gets or sets the ShutdownPromptSeconds - /// - public int ShutdownPromptSeconds { get; set; } + /// + /// Timer used for informing the UIs of a shutdown + /// + public SecondsCountdownTimer ShutdownPromptTimer { get; private set; } - /// - /// Gets or sets the ShutdownVacancySeconds - /// - public int ShutdownVacancySeconds { get; set; } + /// + /// + /// + public int ShutdownPromptSeconds { get; set; } + public int ShutdownVacancySeconds { get; set; } + public eShutdownType ShutdownType { get; private set; } - /// - /// Gets or sets the ShutdownType - /// - public eShutdownType ShutdownType { get; private set; } + public EssentialsRoomEmergencyBase Emergency { get; set; } - /// - /// Gets or sets the Emergency - /// - public EssentialsRoomEmergencyBase Emergency { get; set; } + public Core.Privacy.MicrophonePrivacyController MicrophonePrivacy { get; set; } - /// - /// Gets or sets the MicrophonePrivacy - /// - public Core.Privacy.MicrophonePrivacyController MicrophonePrivacy { get; set; } + public string LogoUrlLightBkgnd { get; set; } - /// - /// Gets or sets the LogoUrlLightBkgnd - /// - public string LogoUrlLightBkgnd { get; set; } + public string LogoUrlDarkBkgnd { get; set; } - /// - /// Gets or sets the LogoUrlDarkBkgnd - /// - public string LogoUrlDarkBkgnd { get; set; } + protected SecondsCountdownTimer RoomVacancyShutdownTimer { get; private set; } - /// - /// Gets or sets the RoomVacancyShutdownTimer - /// - protected SecondsCountdownTimer RoomVacancyShutdownTimer { get; private set; } + public eVacancyMode VacancyMode { get; private set; } - /// - /// Gets or sets the VacancyMode - /// - public eVacancyMode VacancyMode { get; private set; } + /// + /// Seconds after vacancy prompt is displayed until shutdown + /// + protected int RoomVacancyShutdownSeconds; - /// - /// Seconds after vacancy prompt is displayed until shutdown - /// - protected int RoomVacancyShutdownSeconds; + /// + /// Seconds after vacancy detected until prompt is displayed + /// + protected int RoomVacancyShutdownPromptSeconds; - /// - /// Seconds after vacancy detected until prompt is displayed - /// - protected int RoomVacancyShutdownPromptSeconds; - - /// - /// - /// - protected abstract Func OnFeedbackFunc { get; } + /// + /// + /// + protected abstract Func OnFeedbackFunc { get; } /// /// Gets or sets the SavedVolumeLevels @@ -276,362 +205,293 @@ namespace PepperDash.Essentials.Core /// public bool ZeroVolumeWhenSwtichingVolumeDevices { get; private set; } - /// - /// Constructor for EssentialsRoomBase - /// - /// config of the device - public EssentialsRoomBase(DeviceConfig config) - : base(config) + + public EssentialsRoomBase(DeviceConfig config) + : base(config) + { + EnvironmentalControlDevices = new List(); + + // Setup the ShutdownPromptTimer + ShutdownPromptTimer = new SecondsCountdownTimer(Key + "-offTimer"); + ShutdownPromptTimer.IsRunningFeedback.OutputChange += (o, a) => { - EnvironmentalControlDevices = new List(); + if (!ShutdownPromptTimer.IsRunningFeedback.BoolValue) + ShutdownType = eShutdownType.None; + }; - // Setup the ShutdownPromptTimer - ShutdownPromptTimer = new SecondsCountdownTimer(Key + "-offTimer"); - ShutdownPromptTimer.IsRunningFeedback.OutputChange += (o, a) => - { - if (!ShutdownPromptTimer.IsRunningFeedback.BoolValue) - ShutdownType = eShutdownType.None; - }; + ShutdownPromptTimer.HasFinished += (o, a) => Shutdown(); // Shutdown is triggered - ShutdownPromptTimer.HasFinished += (o, a) => Shutdown(); // Shutdown is triggered + ShutdownPromptSeconds = 60; + ShutdownVacancySeconds = 120; + + ShutdownType = eShutdownType.None; - ShutdownPromptSeconds = 60; - ShutdownVacancySeconds = 120; - - ShutdownType = eShutdownType.None; + RoomVacancyShutdownTimer = new SecondsCountdownTimer(Key + "-vacancyOffTimer"); + //RoomVacancyShutdownTimer.IsRunningFeedback.OutputChange += (o, a) => + //{ + // if (!RoomVacancyShutdownTimer.IsRunningFeedback.BoolValue) + // ShutdownType = ShutdownType.Vacancy; + //}; + RoomVacancyShutdownTimer.HasFinished += new EventHandler(RoomVacancyShutdownPromptTimer_HasFinished); // Shutdown is triggered - RoomVacancyShutdownTimer = new SecondsCountdownTimer(Key + "-vacancyOffTimer"); - //RoomVacancyShutdownTimer.IsRunningFeedback.OutputChange += (o, a) => - //{ - // if (!RoomVacancyShutdownTimer.IsRunningFeedback.BoolValue) - // ShutdownType = ShutdownType.Vacancy; - //}; - RoomVacancyShutdownTimer.HasFinished += new EventHandler(RoomVacancyShutdownPromptTimer_HasFinished); // Shutdown is triggered + RoomVacancyShutdownPromptSeconds = 1500; // 25 min to prompt warning + RoomVacancyShutdownSeconds = 240; // 4 min after prompt will trigger shutdown prompt + VacancyMode = eVacancyMode.None; - RoomVacancyShutdownPromptSeconds = 1500; // 25 min to prompt warning - RoomVacancyShutdownSeconds = 240; // 4 min after prompt will trigger shutdown prompt - VacancyMode = eVacancyMode.None; + OnFeedback = new BoolFeedback(OnFeedbackFunc); - OnFeedback = new BoolFeedback(OnFeedbackFunc); + IsWarmingUpFeedback = new BoolFeedback(IsWarmingFeedbackFunc); + IsCoolingDownFeedback = new BoolFeedback(IsCoolingFeedbackFunc); - IsWarmingUpFeedback = new BoolFeedback(IsWarmingFeedbackFunc); - IsCoolingDownFeedback = new BoolFeedback(IsCoolingFeedbackFunc); + AddPostActivationAction(() => + { + if (RoomOccupancy != null) + OnRoomOccupancyIsSet(); + }); + } - AddPostActivationAction(() => - { - if (RoomOccupancy != null) - OnRoomOccupancyIsSet(); - }); + public override bool CustomActivate() + { + SetUpMobileControl(); + + return base.CustomActivate(); + } + + /// + /// Sets the SourceListKey property to the passed in value or the default if no value passed in + /// + /// + protected void SetSourceListKey(string sourceListKey) + { + if (!string.IsNullOrEmpty(sourceListKey)) + { + SourceListKey = sourceListKey; } - - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() + else { - SetUpMobileControl(); - - return base.CustomActivate(); + sourceListKey = _defaultListKey; } + } - /// - /// Sets the SourceListKey property to the passed in value or the default if no value passed in - /// - /// - protected void SetSourceListKey(string sourceListKey) + protected void SetDestinationListKey(string destinationListKey) + { + if (!string.IsNullOrEmpty(destinationListKey)) { - if (!string.IsNullOrEmpty(sourceListKey)) - { - SourceListKey = sourceListKey; - } - else - { - sourceListKey = _defaultListKey; - } + DestinationListKey = destinationListKey; } + } - /// - /// Sets the DestinationListKey property to the passed in value or the default if no value passed in - /// - /// key of the destination list object - protected void SetDestinationListKey(string destinationListKey) + /// + /// If mobile control is enabled, sets the appropriate properties + /// + void SetUpMobileControl() + { + var mcBridgeKey = string.Format("mobileControlBridge-{0}", Key); + var mcBridge = DeviceManager.GetDeviceForKey(mcBridgeKey); + if (mcBridge == null) { - if (!string.IsNullOrEmpty(destinationListKey)) - { - DestinationListKey = destinationListKey; - } + Debug.LogMessage(LogEventLevel.Debug, this, "*********************Mobile Control Bridge Not found for this room."); + IsMobileControlEnabled = false; + return; } - - /// - /// If mobile control is enabled, sets the appropriate properties - /// - void SetUpMobileControl() + else { - var mcBridgeKey = string.Format("mobileControlBridge-{0}", Key); - var mcBridge = DeviceManager.GetDeviceForKey(mcBridgeKey); - if (mcBridge == null) - { - Debug.LogMessage(LogEventLevel.Debug, this, "*********************Mobile Control Bridge Not found for this room."); - IsMobileControlEnabled = false; - return; - } - else - { - MobileControlRoomBridge = mcBridge as IMobileControlRoomMessenger; - Debug.LogMessage(LogEventLevel.Debug, this, "*********************Mobile Control Bridge found and enabled for this room"); - IsMobileControlEnabled = true; - } + MobileControlRoomBridge = mcBridge as IMobileControlRoomMessenger; + Debug.LogMessage(LogEventLevel.Debug, this, "*********************Mobile Control Bridge found and enabled for this room"); + IsMobileControlEnabled = true; } + } - void RoomVacancyShutdownPromptTimer_HasFinished(object sender, EventArgs e) + void RoomVacancyShutdownPromptTimer_HasFinished(object sender, EventArgs e) + { + switch (VacancyMode) { - switch (VacancyMode) - { - case eVacancyMode.None: - StartRoomVacancyTimer(eVacancyMode.InInitialVacancy); + case eVacancyMode.None: + StartRoomVacancyTimer(eVacancyMode.InInitialVacancy); + break; + case eVacancyMode.InInitialVacancy: + StartRoomVacancyTimer(eVacancyMode.InShutdownWarning); + break; + case eVacancyMode.InShutdownWarning: + { + StartShutdown(eShutdownType.Vacancy); + Debug.LogMessage(LogEventLevel.Information, this, "Shutting Down due to vacancy."); break; - case eVacancyMode.InInitialVacancy: - StartRoomVacancyTimer(eVacancyMode.InShutdownWarning); - break; - case eVacancyMode.InShutdownWarning: - { - StartShutdown(eShutdownType.Vacancy); - Debug.LogMessage(LogEventLevel.Information, this, "Shutting Down due to vacancy."); - break; - } - default: - break; - } + } + default: + break; } + } - /// - /// - /// - /// - /// - /// StartShutdown method - /// - public void StartShutdown(eShutdownType type) - { - // Check for shutdowns running. Manual should override other shutdowns + /// + /// + /// + /// + public void StartShutdown(eShutdownType type) + { + // Check for shutdowns running. Manual should override other shutdowns - if (type == eShutdownType.Manual) - ShutdownPromptTimer.SecondsToCount = ShutdownPromptSeconds; - else if (type == eShutdownType.Vacancy) - ShutdownPromptTimer.SecondsToCount = ShutdownVacancySeconds; - ShutdownType = type; - ShutdownPromptTimer.Start(); + if (type == eShutdownType.Manual) + ShutdownPromptTimer.SecondsToCount = ShutdownPromptSeconds; + else if (type == eShutdownType.Vacancy) + ShutdownPromptTimer.SecondsToCount = ShutdownVacancySeconds; + ShutdownType = type; + ShutdownPromptTimer.Start(); - Debug.LogMessage(LogEventLevel.Information, this, "ShutdownPromptTimer Started. Type: {0}. Seconds: {1}", ShutdownType, ShutdownPromptTimer.SecondsToCount); - } + Debug.LogMessage(LogEventLevel.Information, this, "ShutdownPromptTimer Started. Type: {0}. Seconds: {1}", ShutdownType, ShutdownPromptTimer.SecondsToCount); + } - /// - /// StartRoomVacancyTimer method - /// - public void StartRoomVacancyTimer(eVacancyMode mode) - { - if (mode == eVacancyMode.None) - RoomVacancyShutdownTimer.SecondsToCount = RoomVacancyShutdownPromptSeconds; - else if (mode == eVacancyMode.InInitialVacancy) - RoomVacancyShutdownTimer.SecondsToCount = RoomVacancyShutdownSeconds; - else if (mode == eVacancyMode.InShutdownWarning) - RoomVacancyShutdownTimer.SecondsToCount = 60; - VacancyMode = mode; - RoomVacancyShutdownTimer.Start(); + public void StartRoomVacancyTimer(eVacancyMode mode) + { + if (mode == eVacancyMode.None) + RoomVacancyShutdownTimer.SecondsToCount = RoomVacancyShutdownPromptSeconds; + else if (mode == eVacancyMode.InInitialVacancy) + RoomVacancyShutdownTimer.SecondsToCount = RoomVacancyShutdownSeconds; + else if (mode == eVacancyMode.InShutdownWarning) + RoomVacancyShutdownTimer.SecondsToCount = 60; + VacancyMode = mode; + RoomVacancyShutdownTimer.Start(); - Debug.LogMessage(LogEventLevel.Information, this, "Vacancy Timer Started. Mode: {0}. Seconds: {1}", VacancyMode, RoomVacancyShutdownTimer.SecondsToCount); - } + Debug.LogMessage(LogEventLevel.Information, this, "Vacancy Timer Started. Mode: {0}. Seconds: {1}", VacancyMode, RoomVacancyShutdownTimer.SecondsToCount); + } - /// - /// Shutdown method - /// - public void Shutdown() - { - VacancyMode = eVacancyMode.None; - EndShutdown(); - } + /// + /// Resets the vacancy mode and shutsdwon the room + /// + public void Shutdown() + { + VacancyMode = eVacancyMode.None; + EndShutdown(); + } - /// - /// This method is for the derived class to define it's specific shutdown - /// requirements but should not be called directly. It is called by Shutdown() - /// - protected abstract void EndShutdown(); + /// + /// This method is for the derived class to define it's specific shutdown + /// requirements but should not be called directly. It is called by Shutdown() + /// + protected abstract void EndShutdown(); - /// - /// Override this to implement a default volume level(s) method - /// - public abstract void SetDefaultLevels(); + /// + /// Override this to implement a default volume level(s) method + /// + public abstract void SetDefaultLevels(); - /// - /// Sets the object to be used as the IOccupancyStatusProvider for the room. Can be an Occupancy Aggregator or a specific device - /// - /// - /// - public void SetRoomOccupancy(IOccupancyStatusProvider statusProvider, int timeoutMinutes) - { + /// + /// Sets the object to be used as the IOccupancyStatusProvider for the room. Can be an Occupancy Aggregator or a specific device + /// + /// + public void SetRoomOccupancy(IOccupancyStatusProvider statusProvider, int timeoutMinutes) + { if (statusProvider == null) { Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Occupancy sensor device is null"); return; } - Debug.LogMessage(LogEventLevel.Information, this, "Room Occupancy set to device: '{0}'", (statusProvider as Device).Key); - Debug.LogMessage(LogEventLevel.Information, this, "Timeout Minutes from Config is: {0}", timeoutMinutes); + Debug.LogMessage(LogEventLevel.Information, this, "Room Occupancy set to device: '{0}'", (statusProvider as Device).Key); + 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.IEssentialsRoomFusionController) - OccupancyStatusProviderIsRemote = true; + // If status provider is fusion, set flag to remote + if (statusProvider is Core.Fusion.IEssentialsRoomFusionController) + OccupancyStatusProviderIsRemote = true; - if(timeoutMinutes > 0) - RoomVacancyShutdownSeconds = timeoutMinutes * 60; + if(timeoutMinutes > 0) + RoomVacancyShutdownSeconds = timeoutMinutes * 60; - Debug.LogMessage(LogEventLevel.Information, this, "RoomVacancyShutdownSeconds set to {0}", RoomVacancyShutdownSeconds); + Debug.LogMessage(LogEventLevel.Information, this, "RoomVacancyShutdownSeconds set to {0}", RoomVacancyShutdownSeconds); - RoomOccupancy = statusProvider; + RoomOccupancy = statusProvider; - RoomOccupancy.RoomIsOccupiedFeedback.OutputChange -= RoomIsOccupiedFeedback_OutputChange; - RoomOccupancy.RoomIsOccupiedFeedback.OutputChange += RoomIsOccupiedFeedback_OutputChange; + RoomOccupancy.RoomIsOccupiedFeedback.OutputChange -= RoomIsOccupiedFeedback_OutputChange; + RoomOccupancy.RoomIsOccupiedFeedback.OutputChange += RoomIsOccupiedFeedback_OutputChange; - OnRoomOccupancyIsSet(); - } - - void OnRoomOccupancyIsSet() - { - var handler = RoomOccupancyIsSet; - if (handler != null) - handler(this, new EventArgs()); - } - - /// - /// To allow base class to power room on to last source - /// - public abstract void PowerOnToDefaultOrLastSource(); - - /// - /// To allow base class to power room on to default source - /// - /// - public abstract bool RunDefaultPresentRoute(); - - void RoomIsOccupiedFeedback_OutputChange(object sender, EventArgs e) - { - if (RoomOccupancy.RoomIsOccupiedFeedback.BoolValue == false && AllowVacancyTimerToStart()) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Notice: Vacancy Detected"); - // Trigger the timer when the room is vacant - StartRoomVacancyTimer(eVacancyMode.InInitialVacancy); - } - else - { - Debug.LogMessage(LogEventLevel.Debug, this, "Notice: Occupancy Detected"); - // Reset the timer when the room is occupied - RoomVacancyShutdownTimer.Cancel(); - } - } - - /// - /// Executes when RoomVacancyShutdownTimer expires. Used to trigger specific room actions as needed. Must nullify the timer object when executed - /// - /// - public abstract void RoomVacatedForTimeoutPeriod(object o); - - /// - /// Allow the vacancy event from an occupancy sensor to turn the room off. - /// - /// If the timer should be allowed. Defaults to true - protected virtual bool AllowVacancyTimerToStart() - { - return true; - } + OnRoomOccupancyIsSet(); } - - /// - /// To describe the various ways a room may be shutting down - /// - public enum eShutdownType + + void OnRoomOccupancyIsSet() { - /// - /// No shutdown in progress - /// - None = 0, - - /// - /// Manual shutdown initiated - /// - External, - - /// - /// Vacancy based shutdown - /// - Manual, - - /// - /// Shutdown due to room vacancy - /// - Vacancy + var handler = RoomOccupancyIsSet; + if (handler != null) + handler(this, new EventArgs()); } /// - /// Enumeration of eVacancyMode values + /// To allow base class to power room on to last source /// - public enum eVacancyMode - { - /// - /// No vacancy detected - /// - None = 0, - - /// - /// InInitialVacancy - countdown to warning - /// - InInitialVacancy, - - /// - /// InShutdownWarning - countdown to shutdown - /// - InShutdownWarning - } + public abstract void PowerOnToDefaultOrLastSource(); /// - /// Enumeration of eWarmingCoolingMode values + /// To allow base class to power room on to default source /// - public enum eWarmingCoolingMode + /// + public abstract bool RunDefaultPresentRoute(); + + void RoomIsOccupiedFeedback_OutputChange(object sender, EventArgs e) { - /// - /// None - /// - None, - - /// - /// Warming - /// - Warming, - - /// - /// Cooling - /// - Cooling - } - - /// - /// Base class for room emergency implementations - /// - public abstract class EssentialsRoomEmergencyBase : IKeyed - { - /// - /// Key of the room - /// - public string Key { get; private set; } - - /// - /// Constructor for EssentialsRoomEmergencyBase - /// - /// key of the room - public EssentialsRoomEmergencyBase(string key) + if (RoomOccupancy.RoomIsOccupiedFeedback.BoolValue == false && AllowVacancyTimerToStart()) { - Key = key; + Debug.LogMessage(LogEventLevel.Debug, this, "Notice: Vacancy Detected"); + // Trigger the timer when the room is vacant + StartRoomVacancyTimer(eVacancyMode.InInitialVacancy); } + else + { + Debug.LogMessage(LogEventLevel.Debug, this, "Notice: Occupancy Detected"); + // Reset the timer when the room is occupied + RoomVacancyShutdownTimer.Cancel(); + } + } + + /// + /// Executes when RoomVacancyShutdownTimer expires. Used to trigger specific room actions as needed. Must nullify the timer object when executed + /// + /// + public abstract void RoomVacatedForTimeoutPeriod(object o); + + /// + /// Allow the vacancy event from an occupancy sensor to turn the room off. + /// + /// If the timer should be allowed. Defaults to true + protected virtual bool AllowVacancyTimerToStart() + { + return true; + } +} + +/// +/// To describe the various ways a room may be shutting down +/// +public enum eShutdownType +{ + None = 0, + External, + Manual, + Vacancy +} + +public enum eVacancyMode +{ + None = 0, + InInitialVacancy, + InShutdownWarning +} + +/// +/// +/// +public enum eWarmingCoolingMode +{ + None, + Warming, + Cooling +} + +public abstract class EssentialsRoomEmergencyBase : IKeyed +{ + public string Key { get; private set; } + + public EssentialsRoomEmergencyBase(string key) + { + Key = key; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/IEssentialsRoom.cs b/src/PepperDash.Essentials.Core/Room/IEssentialsRoom.cs index 3ebe28c4..681b6675 100644 --- a/src/PepperDash.Essentials.Core/Room/IEssentialsRoom.cs +++ b/src/PepperDash.Essentials.Core/Room/IEssentialsRoom.cs @@ -10,103 +10,101 @@ using PepperDash.Essentials.Core.Devices; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Describes the basic functionality of an EssentialsRoom +/// +public interface IEssentialsRoom : IKeyName, IReconfigurableDevice, IRunDefaultPresentRoute, IEnvironmentalControls { /// - /// Describes the basic functionality of an EssentialsRoom + /// Gets the PowerFeedback /// - public interface IEssentialsRoom : IKeyName, IReconfigurableDevice, IRunDefaultPresentRoute, IEnvironmentalControls - { - /// - /// Gets the PowerFeedback - /// - BoolFeedback OnFeedback { get; } + BoolFeedback OnFeedback { get; } - /// - /// Gets the IsOccupiedFeedback - /// - BoolFeedback IsWarmingUpFeedback { get; } + /// + /// Gets the IsOccupiedFeedback + /// + BoolFeedback IsWarmingUpFeedback { get; } - /// - /// Gets the IsCoolingDownFeedback - /// - BoolFeedback IsCoolingDownFeedback { get; } + /// + /// Gets the IsCoolingDownFeedback + /// + BoolFeedback IsCoolingDownFeedback { get; } - /// - /// Gets a value indicating whether mobile control is enabled for this room - /// - bool IsMobileControlEnabled { get; } + /// + /// Gets a value indicating whether mobile control is enabled for this room + /// + bool IsMobileControlEnabled { get; } - /// - /// Gets the MobileControlRoomBridge - /// - IMobileControlRoomMessenger MobileControlRoomBridge { get; } + /// + /// Gets the MobileControlRoomBridge + /// + IMobileControlRoomMessenger MobileControlRoomBridge { get; } - /// - /// Gets the SourceListKey - /// - string SourceListKey { get; } + /// + /// Gets the SourceListKey + /// + string SourceListKey { get; } - /// - /// Gets the DestinationListKey - /// - string DestinationListKey { get; } + /// + /// Gets the DestinationListKey + /// + string DestinationListKey { get; } - /// - /// Gets the AudioControlPointListKey - /// - string AudioControlPointListKey { get; } + /// + /// Gets the AudioControlPointListKey + /// + string AudioControlPointListKey { get; } - /// - /// Gets the CameraListKey - /// - string CameraListKey { get; } + /// + /// Gets the CameraListKey + /// + string CameraListKey { get; } - /// - /// Gets the ShutdownPromptTimer - /// - SecondsCountdownTimer ShutdownPromptTimer { get; } + /// + /// Gets the ShutdownPromptTimer + /// + SecondsCountdownTimer ShutdownPromptTimer { get; } - /// - /// Gets the ShutdownVacancyTimer - /// - int ShutdownPromptSeconds { get; } + /// + /// Gets the ShutdownVacancyTimer + /// + int ShutdownPromptSeconds { get; } - /// - /// Gets the ShutdownVacancySeconds - /// - int ShutdownVacancySeconds { get; } + /// + /// Gets the ShutdownVacancySeconds + /// + int ShutdownVacancySeconds { get; } - /// - /// Gets the ShutdownType - /// - eShutdownType ShutdownType { get; } + /// + /// Gets the ShutdownType + /// + eShutdownType ShutdownType { get; } - /// - /// Gets the LogoUrlLightBkgnd - /// - string LogoUrlLightBkgnd { get; } + /// + /// Gets the LogoUrlLightBkgnd + /// + string LogoUrlLightBkgnd { get; } - /// - /// Gets the LogoUrlDarkBkgnd - /// - string LogoUrlDarkBkgnd { get; } + /// + /// Gets the LogoUrlDarkBkgnd + /// + string LogoUrlDarkBkgnd { get; } - /// - /// Starts the shutdown process - /// - /// type of shutdown event - void StartShutdown(eShutdownType type); + /// + /// Starts the shutdown process + /// + /// type of shutdown event + void StartShutdown(eShutdownType type); - /// - /// Shuts down the room - /// - void Shutdown(); + /// + /// Shuts down the room + /// + void Shutdown(); - /// - /// Powers on the room to either the default source or the last source used - /// - void PowerOnToDefaultOrLastSource(); - } - -} \ No newline at end of file + /// + /// Powers on the room to either the default source or the last source used + /// + void PowerOnToDefaultOrLastSource(); +} diff --git a/src/PepperDash.Essentials.Core/Room/IRoomEventSchedule.cs b/src/PepperDash.Essentials.Core/Room/IRoomEventSchedule.cs index a42ea505..8f841def 100644 --- a/src/PepperDash.Essentials.Core/Room/IRoomEventSchedule.cs +++ b/src/PepperDash.Essentials.Core/Room/IRoomEventSchedule.cs @@ -2,39 +2,40 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Defines the contract for IRoomEventSchedule +/// +public interface IRoomEventSchedule { /// - /// Defines the contract for IRoomEventSchedule + /// Adds or updates a scheduled event /// - public interface IRoomEventSchedule - { - /// - /// Adds or updates a scheduled event - /// - /// - void AddOrUpdateScheduledEvent(ScheduledEventConfig eventConfig); - - /// - /// Removes a scheduled event by its key - /// - /// - List GetScheduledEvents(); - - /// - /// Removes a scheduled event by its key - /// - event EventHandler ScheduledEventsChanged; - } + /// + void AddOrUpdateScheduledEvent(ScheduledEventConfig eventConfig); /// - /// Represents a ScheduledEventEventArgs + /// Removes a scheduled event by its key /// - public class ScheduledEventEventArgs : EventArgs - { - /// - /// Gets or sets the ScheduledEvents - /// - public List ScheduledEvents; - } + /// + List GetScheduledEvents(); + + /// + /// Removes a scheduled event by its key + /// + event EventHandler ScheduledEventsChanged; } + +/// +/// Represents a ScheduledEventEventArgs +/// +public class ScheduledEventEventArgs : EventArgs +{ + /// + /// Gets or sets the ScheduledEvents + /// + public List ScheduledEvents; +} + diff --git a/src/PepperDash.Essentials.Core/Room/Interfaces.cs b/src/PepperDash.Essentials.Core/Room/Interfaces.cs index 0d2f0850..c0041a9a 100644 --- a/src/PepperDash.Essentials.Core/Room/Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Room/Interfaces.cs @@ -4,307 +4,308 @@ using System.Collections.Generic; using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// For rooms with in call feedback +/// +public interface IHasInCallFeedback { /// - /// For rooms with in call feedback + /// Feedback indicating whether the room is currently in a call /// - public interface IHasInCallFeedback - { - /// - /// Gets the InCallFeedback - /// - BoolFeedback InCallFeedback { get; } - } + BoolFeedback InCallFeedback { get; } +} + +/// +/// For rooms with a single display +/// +public interface IHasDefaultDisplay +{ + /// + /// The default display for the room, used for presentation routing and other default routes + /// + IRoutingSink DefaultDisplay { get; } +} + +/// +/// For rooms with multiple displays +/// +[Obsolete("This interface is being deprecated in favor of using destination lists for routing to multiple displays.")] +public interface IHasMultipleDisplays +{ + /// + /// A dictionary of displays in the room with the key being the type of display (presentation, calling, etc.) and the value being the display device + /// + Dictionary Displays { get; } +} + +/// +/// For rooms with routing +/// +public interface IRunRouteAction +{ + /// + /// Runs a route action for a given route and source list key. + /// The source list key is used to determine the source for the route from the source list in the room's routing controller. + /// This allows for dynamic routing based on what source is selected in the UI for a given route. + /// + /// + /// + void RunRouteAction(string routeKey, string sourceListKey); /// - /// For rooms with a single display + /// Runs a route action for a given route and source list key with a callback for success. + /// The source list key is used to determine the source for the route from the source list in the room's routing controller. + /// This allows for dynamic routing based on what source is selected in the UI for a given route. /// - public interface IHasDefaultDisplay - { - /// - /// Gets the DefaultDisplay - /// - IRoutingSink DefaultDisplay { get; } - } + /// + /// + /// + void RunRouteAction(string routeKey, string sourceListKey, Action successCallback); +} + +/// +/// Simplified routing direct from source to destination +/// +public interface IRunDirectRouteAction +{ + /// + /// Runs a direct route from a source to a destination with an optional signal type for routing. + /// + /// + /// + /// + void RunDirectRoute(string sourceKey, string destinationKey, eRoutingSignalType type = eRoutingSignalType.AudioVideo); +} + +/// +/// Describes a room with matrix routing +/// +public interface IHasMatrixRouting : IHasRoutingEndpoints +{ + /// + /// The key of the matrix routing device in the room + /// + string MatrixRoutingDeviceKey { get; } +} + +/// +/// Describes a room with routing endpoints +/// +public interface IHasRoutingEndpoints +{ + /// + /// The keys of the endpoints in the room used for routing + /// + List EndpointKeys { get; } +} + +/// +/// Describes a room with a shutdown prompt timer +/// +public interface IShutdownPromptTimer +{ + /// + /// The shutdown prompt timer for the room + /// + SecondsCountdownTimer ShutdownPromptTimer { get; } /// - /// For rooms with multiple displays + /// Sets the number of seconds for the shutdown prompt timer /// - [Obsolete("Will be removed in a future version")] - public interface IHasMultipleDisplays - { - /// - /// Gets the Displays dictionary - /// - Dictionary Displays { get; } - } + /// + void SetShutdownPromptSeconds(int seconds); /// - /// For rooms with routing + /// Starts the shutdown process for the room with a given shutdown type /// - public interface IRunRouteAction - { - /// - /// Runs a route action - /// - /// - /// - void RunRouteAction(string routeKey, string sourceListKey); + /// + void StartShutdown(eShutdownType type); +} - /// - /// Runs a route action with a success callback - /// - /// - /// - /// - void RunRouteAction(string routeKey, string sourceListKey, Action successCallback); - } +/// +/// Describes a room with a tech password +/// +public interface ITechPassword +{ + /// + /// Event that fires with the result of validating a tech password + /// + event EventHandler TechPasswordValidateResult; /// - /// Defines the contract for IRunDirectRouteAction + /// Event that fires when the tech password is changed /// - public interface IRunDirectRouteAction - { - /// - /// Runs a direct route - /// - /// - /// - /// - void RunDirectRoute(string sourceKey, string destinationKey, eRoutingSignalType type = eRoutingSignalType.AudioVideo); - } + event EventHandler TechPasswordChanged; /// - /// Describes a room with matrix routing + /// The length of the tech password /// - public interface IHasMatrixRouting - { - /// - /// Gets the MatrixRoutingDeviceKey - /// - string MatrixRoutingDeviceKey { get; } - - /// - /// Gets the EndpointKeys - /// - List EndpointKeys { get; } - } + int TechPasswordLength { get; } /// - /// Defines the contract for IHasRoutingEndpoints + /// Validates a given tech password against the current tech password for the room. Fires the TechPasswordValidateResult event with the result of the validation. /// - public interface IHasRoutingEndpoints - { - /// - /// Gets the EndpointKeys - /// - List EndpointKeys { get; } - } + /// + void ValidateTechPassword(string password); /// - /// Describes a room with a shutdown prompt timer + /// Sets a new tech password for the room. Fires the TechPasswordChanged event when the password is successfully changed. /// - public interface IShutdownPromptTimer - { - /// - /// Gets the ShutdownPromptTimer - /// - SecondsCountdownTimer ShutdownPromptTimer { get; } + /// + /// + void SetTechPassword(string oldPassword, string newPassword); +} - /// - /// Gets the ShutdownPromptSeconds - /// - /// number of seconds to set - void SetShutdownPromptSeconds(int seconds); - - /// - /// Starts the shutdown process - /// - /// type of shutdown event - void StartShutdown(eShutdownType type); - } +/// +/// Event args for tech password validation results +/// +public class TechPasswordEventArgs : EventArgs +{ + /// + /// Indicates whether the tech password validation was successful or not + /// + public bool IsValid { get; private set; } /// - /// Defines the contract for ITechPassword + /// Constructor /// - public interface ITechPassword + /// + public TechPasswordEventArgs(bool isValid) { - /// - /// Event fired when tech password validation result is available - /// - event EventHandler TechPasswordValidateResult; - - /// - /// Event fired when tech password is changed - /// - event EventHandler TechPasswordChanged; - - /// - /// Gets the TechPasswordLength - /// - int TechPasswordLength { get; } - - /// - /// Validates the tech password - /// - /// The tech password to validate - void ValidateTechPassword(string password); - - /// - /// Sets the tech password - /// - /// The current tech password - /// The new tech password to set - void SetTechPassword(string oldPassword, string newPassword); + IsValid = isValid; } +} + +/// +/// For rooms that default presentation only routing +/// +public interface IRunDefaultPresentRoute +{ + /// + /// Runs the default presentation route for the room. This is typically used for a "Present" button in the UI that routes a selected source to the default presentation display without needing to specify the source or destination for the route. + /// + /// True if the route was successfully run, false otherwise. + bool RunDefaultPresentRoute(); +} + +/// +/// For rooms that have default presentation and calling routes +/// +public interface IRunDefaultCallRoute : IRunDefaultPresentRoute +{ + /// + /// Runs the default call route for the room. This is typically used for a "Call" button in the UI that routes a selected source to the default call display without needing to specify the source or destination for the route. + /// + /// True if the route was successfully run, false otherwise. + bool RunDefaultCallRoute(); +} + +/// +/// Describes environmental controls available on a room such as lighting, shades, temperature, etc. +/// +public interface IEnvironmentalControls +{ + /// + /// A list of devices in the room that can be used for environmental control such as lighting, shades, temperature, etc. + /// + List EnvironmentalControlDevices { get; } /// - /// Represents a TechPasswordEventArgs + /// Indicates whether the room has any devices that can be used for environmental control such as lighting, shades, temperature, etc. /// - public class TechPasswordEventArgs : EventArgs - { - /// - /// Gets or sets the IsValid - /// - public bool IsValid { get; private set; } + bool HasEnvironmentalControlDevices { get; } +} - /// - /// Constructor for TechPasswordEventArgs - /// - /// - public TechPasswordEventArgs(bool isValid) - { - IsValid = isValid; - } - } +/// +/// Describes a room with occupancy status +/// +public interface IRoomOccupancy:IKeyed +{ + /// + /// The occupancy status provider for the room, which provides the current occupancy status of the room based on a sensor or other input. + /// This is used for features such as automatic shutdown when the room is vacant, adjusting environmental controls based on occupancy, and other occupancy-based automation. + /// + IOccupancyStatusProvider RoomOccupancy { get; } /// - /// Defines the contract for IRunDefaultPresentRoute + /// Indicates whether the occupancy status provider for the room is remote, meaning it is not directly connected to the control system and may require additional setup or configuration to work properly. + /// This can be used to determine whether certain features that rely on occupancy status should be enabled or disabled for the room. /// - public interface IRunDefaultPresentRoute - { - /// - /// Runs the default present route - /// - /// - bool RunDefaultPresentRoute(); - } + bool OccupancyStatusProviderIsRemote { get; } /// - /// For rooms that have default presentation and calling routes + /// Sets the occupancy status provider for the room with a given timeout period for determining when the room is vacant. T + /// his is used to configure the occupancy-based features of the room based on the specific occupancy sensor or input being used and the desired timeout period for determining vacancy. /// - public interface IRunDefaultCallRoute : IRunDefaultPresentRoute - { - /// - /// Runs the default call route - /// - /// - bool RunDefaultCallRoute(); - } + /// + /// + void SetRoomOccupancy(IOccupancyStatusProvider statusProvider, int timeoutMinutes); /// - /// Describes environmental controls available on a room such as lighting, shades, temperature, etc. + /// Event handler for when the room is vacated for the timeout period, which can be used to trigger actions such as shutting down the room, adjusting environmental controls, or other automation based on vacancy. /// - public interface IEnvironmentalControls - { - /// - /// Gets the EnvironmentalControlDevices - /// - List EnvironmentalControlDevices { get; } - - /// - /// Gets a value indicating whether the room has environmental control devices - /// - bool HasEnvironmentalControlDevices { get; } - } + /// + void RoomVacatedForTimeoutPeriod(object o); /// - /// Defines the contract for IRoomOccupancy + /// Starts the timer for determining when the room is vacant based on the specified vacancy mode, which can be used to trigger actions such as shutting down the room, adjusting environmental controls, or other automation based on vacancy. /// - public interface IRoomOccupancy : IKeyed - { - /// - /// Gets the RoomOccupancy - /// - IOccupancyStatusProvider RoomOccupancy { get; } - - /// - /// Gets a value indicating whether the OccupancyStatusProviderIsRemote - /// - bool OccupancyStatusProviderIsRemote { get; } - - /// - /// Sets the room occupancy - /// - /// - /// - void SetRoomOccupancy(IOccupancyStatusProvider statusProvider, int timeoutMinutes); - - /// - /// Called when the room has been vacated for the timeout period - /// - /// - void RoomVacatedForTimeoutPeriod(object o); - - /// - /// Starts the room vacancy timer - /// - /// vacancy mode - void StartRoomVacancyTimer(eVacancyMode mode); - - /// - /// Gets the VacancyMode - /// - eVacancyMode VacancyMode { get; } - - /// - /// Event fired when room occupancy is set - /// - event EventHandler RoomOccupancyIsSet; - } + /// + void StartRoomVacancyTimer(eVacancyMode mode); /// - /// Defines the contract for IEmergency + /// Gets the current vacancy mode for the room, which indicates the current state of the room in terms of occupancy and can be used to determine whether certain features or automation should be enabled or disabled based on vacancy. /// - public interface IEmergency - { - /// - /// Gets the Emergency - /// - EssentialsRoomEmergencyBase Emergency { get; } - } + eVacancyMode VacancyMode { get; } /// - /// Defines the contract for IMicrophonePrivacy + /// Event that fires when the occupancy status for the room is set, which can be used to trigger actions such as updating the UI, adjusting environmental controls, or other automation based on occupancy status changes. /// - public interface IMicrophonePrivacy - { - /// - /// Gets the MicrophonePrivacy - /// - Core.Privacy.MicrophonePrivacyController MicrophonePrivacy { get; } - } + event EventHandler RoomOccupancyIsSet; +} +/// +/// Describes a room with emergency features +/// +public interface IEmergency +{ /// - /// Defines the contract for IHasAccessoryDevices + /// The emergency base for the room, which provides access to emergency features such as triggering an emergency alert, contacting emergency services, or other emergency-related functionality. /// - public interface IHasAccessoryDevices : IKeyName - { - /// - /// Gets the AccessoryDeviceKeys - /// - List AccessoryDeviceKeys { get; } - } + EssentialsRoomEmergencyBase Emergency { get; } +} +/// +/// Describes a room with a microphone privacy controller +/// +public interface IMicrophonePrivacy +{ /// - /// Defines the contract for IHasCiscoNavigatorTouchpanel + /// The microphone privacy controller for the room, which provides access to features such as muting or unmuting microphones for privacy purposes. /// - public interface IHasCiscoNavigatorTouchpanel - { - /// - /// Gets the CiscoNavigatorTouchpanelKey - /// - string CiscoNavigatorTouchpanelKey { get; } - } + Core.Privacy.MicrophonePrivacyController MicrophonePrivacy { get; } +} + +/// +/// Describes a room with a camera privacy controller +/// +public interface IHasAccessoryDevices : IKeyName +{ + /// + /// A list of keys for accessory devices in the room such as cameras, microphones, or other devices that are not part of the main control system but can be integrated for additional functionality. + /// + List AccessoryDeviceKeys { get; } +} + +/// +/// Describes a room wih a Cisco Navigator touchpanel +/// +public interface IHasCiscoNavigatorTouchpanel +{ + /// + /// The key for the Cisco Navigator touchpanel in the room, which can be used to route calls or content to the touchpanel or for other integration purposes. + /// + string CiscoNavigatorTouchpanelKey { get; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Room.cs b/src/PepperDash.Essentials.Core/Room/Room.cs index d9c51c9d..7ce1571c 100644 --- a/src/PepperDash.Essentials.Core/Room/Room.cs +++ b/src/PepperDash.Essentials.Core/Room/Room.cs @@ -8,8 +8,8 @@ using Crestron.SimplSharpPro; using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + //*************************************************************************************************** /// @@ -87,15 +87,14 @@ namespace PepperDash.Essentials.Core { get { - return new FeedbackCollection - { - RoomIsOnFeedback, - IsCoolingDownFeedback, - IsWarmingUpFeedback - }; + return new FeedbackCollection + { + RoomIsOnFeedback, + IsCoolingDownFeedback, + IsWarmingUpFeedback + }; } } #endregion - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/iOccupancyStatusProvider.cs b/src/PepperDash.Essentials.Core/Room/iOccupancyStatusProvider.cs index 8ea858ee..28dfcd26 100644 --- a/src/PepperDash.Essentials.Core/Room/iOccupancyStatusProvider.cs +++ b/src/PepperDash.Essentials.Core/Room/iOccupancyStatusProvider.cs @@ -6,16 +6,16 @@ using Crestron.SimplSharp; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Defines the contract for IOccupancyStatusProvider +/// +public interface IOccupancyStatusProvider { /// - /// Defines the contract for IOccupancyStatusProvider + /// Gets the RoomIsOccupiedFeedback /// - public interface IOccupancyStatusProvider - { - /// - /// Gets the RoomIsOccupiedFeedback - /// - BoolFeedback RoomIsOccupiedFeedback { get; } - } -} \ No newline at end of file + BoolFeedback RoomIsOccupiedFeedback { get; } +} diff --git a/src/PepperDash.Essentials.Core/Routing/DummyRoutingInputsDevice.cs b/src/PepperDash.Essentials.Core/Routing/DummyRoutingInputsDevice.cs index 6d127622..79854d3c 100644 --- a/src/PepperDash.Essentials.Core/Routing/DummyRoutingInputsDevice.cs +++ b/src/PepperDash.Essentials.Core/Routing/DummyRoutingInputsDevice.cs @@ -1,39 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; +using PepperDash.Core; -using PepperDash.Core; +namespace PepperDash.Essentials.Core.Routing; -namespace PepperDash.Essentials.Core.Routing +/// +/// Represents a DummyRoutingInputsDevice +/// +public class DummyRoutingInputsDevice : Device, IRoutingSource, IRoutingOutputs { - /// - /// Represents a DummyRoutingInputsDevice - /// - public class DummyRoutingInputsDevice : Device, IRoutingSource, IRoutingOutputs + /// + /// Gets or sets the AudioVideoOutputPort + /// + public RoutingOutputPort AudioVideoOutputPort { get; private set; } + + /// + /// contains the output port + /// + public RoutingPortCollection OutputPorts { - /// - /// Gets or sets the AudioVideoOutputPort - /// - public RoutingOutputPort AudioVideoOutputPort { get; private set; } + get { return new RoutingPortCollection() { AudioVideoOutputPort }; } + } - /// - /// contains the output port - /// - public RoutingPortCollection OutputPorts - { - get { return new RoutingPortCollection() { AudioVideoOutputPort }; } - } - - /// - /// constructor - /// - /// key for special device - public DummyRoutingInputsDevice(string key) : base(key) - { - AudioVideoOutputPort = new RoutingOutputPort("internal", eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.BackplaneOnly, - null, this, true); - } + /// + /// constructor + /// + /// key for special device + public DummyRoutingInputsDevice(string key) : base(key) + { + AudioVideoOutputPort = new RoutingOutputPort("internal", eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.BackplaneOnly, + null, this, true); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 3797f5e2..b4335074 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -2,447 +2,436 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Crestron.SimplSharpPro.Keypads; using PepperDash.Essentials.Core.Queues; using PepperDash.Essentials.Core.Routing; using Serilog.Events; using Debug = PepperDash.Core.Debug; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Extensions added to any IRoutingInputs classes to provide discovery-based routing +/// on those destinations. +/// +public static class Extensions { + /// + /// Stores pending route requests, keyed by the destination device key. + /// Used primarily to handle routing requests while a device is cooling down. + /// + private static readonly Dictionary RouteRequests = new Dictionary(); /// - /// Extensions added to any IRoutingInputs classes to provide discovery-based routing - /// on those destinations. + /// A queue to process route requests and releases sequentially. /// - public static class Extensions + private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue"); + + /// + /// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute + /// and then attempts a new Route and if sucessful, stores that RouteDescriptor + /// in RouteDescriptorCollection.DefaultCollection + /// + public static void ReleaseAndMakeRoute(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, string destinationPortKey = "", string sourcePortKey = "") { - /// - /// Stores pending route requests, keyed by the destination device key. - /// Used primarily to handle routing requests while a device is cooling down. - /// - private static readonly Dictionary RouteRequests = new Dictionary(); + // Remove this line before committing!!!!! + var frame = new StackFrame(1, true); + Debug.LogMessage(LogEventLevel.Information, "ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey); - /// - /// A queue to process route requests and releases sequentially. - /// - private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue"); + var inputPort = string.IsNullOrEmpty(destinationPortKey) ? null : destination.InputPorts.FirstOrDefault(p => p.Key == destinationPortKey); + var outputPort = string.IsNullOrEmpty(sourcePortKey) ? null : source.OutputPorts.FirstOrDefault(p => p.Key == sourcePortKey); - /// - /// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute - /// and then attempts a new Route and if sucessful, stores that RouteDescriptor - /// in RouteDescriptorCollection.DefaultCollection - /// - public static void ReleaseAndMakeRoute(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, string destinationPortKey = "", string sourcePortKey = "") + ReleaseAndMakeRoute(destination, source, signalType, inputPort, outputPort); + } + + /// + /// Will release the existing route to the destination, if a route is found. This does not CLEAR the route, only stop counting usage time on any output ports that have a usage tracker set + /// + /// destination to clear + public static void ReleaseRoute(this IRoutingInputs destination) + { + routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, string.Empty, false)); + } + + /// + /// Will release the existing route to the destination, if a route is found. This does not CLEAR the route, only stop counting usage time on any output ports that have a usage tracker set + /// + /// destination to clear + /// Input to use to find existing route + public static void ReleaseRoute(this IRoutingInputs destination, string inputPortKey) + { + routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, inputPortKey, false)); + } + + /// + /// Clears the route on the destination. This will remove any routes that are currently in use + /// + /// Destination + public static void ClearRoute(this IRoutingInputs destination) + { + routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, string.Empty, true)); + } + + /// + /// Clears the route on the destination. This will remove any routes that are currently in use + /// + /// destination + /// input to use to find existing route + public static void ClearRoute(this IRoutingInputs destination, string inputPortKey) + { + routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, inputPortKey, true)); + } + + /// + /// Removes the route request for the destination. This will remove any routes that are currently in use + /// + /// destination device key + public static void RemoveRouteRequestForDestination(string destinationKey) + { + Debug.LogMessage(LogEventLevel.Information, "Removing route request for {destination}", null, destinationKey); + + var result = RouteRequests.Remove(destinationKey); + + var messageTemplate = result ? "Route Request for {destination} removed" : "Route Request for {destination} not found"; + + Debug.LogMessage(LogEventLevel.Information, messageTemplate, null, destinationKey); + } + + /// + /// Builds a RouteDescriptor that contains the steps necessary to make a route between devices. + /// Routes of type AudioVideo will be built as two separate routes, audio and video. If + /// a route is discovered, a new RouteDescriptor is returned. If one or both parts + /// of an audio/video route are discovered a route descriptor is returned. If no route is + /// discovered, then null is returned + /// + public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) + { + // if it's a single signal type, find the route + if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) && + !(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio))) { - // Remove this line before committing!!!!! - var frame = new StackFrame(1, true); - Debug.LogMessage(LogEventLevel.Information, "ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey); + var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType); + Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key, signalType); - var inputPort = string.IsNullOrEmpty(destinationPortKey) ? null : destination.InputPorts.FirstOrDefault(p => p.Key == destinationPortKey); - var outputPort = string.IsNullOrEmpty(sourcePortKey) ? null : source.OutputPorts.FirstOrDefault(p => p.Key == sourcePortKey); + if (!destination.GetRouteToSource(source, null, null, signalType, 0, singleTypeRouteDescriptor, destinationPort, sourcePort)) + singleTypeRouteDescriptor = null; - ReleaseAndMakeRoute(destination, source, signalType, inputPort, outputPort); - } - - /// - /// Will release the existing route to the destination, if a route is found. This does not CLEAR the route, only stop counting usage time on any output ports that have a usage tracker set. - /// - /// destination to clear - public static void ReleaseRoute(this IRoutingInputs destination) - { - routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, string.Empty, false)); - } - - /// - /// Will release the existing route to the destination, if a route is found. This does not CLEAR the route, only stop counting usage time on any output ports that have a usage tracker set - /// - /// destination to clear - /// Input to use to find existing route - /// - /// ReleaseRoute method - /// - public static void ReleaseRoute(this IRoutingInputs destination, string inputPortKey) - { - routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, inputPortKey, false)); - } - - /// - /// Clears the route on the destination. This will remove any routes that are currently in use - /// - /// Destination - public static void ClearRoute(this IRoutingInputs destination) - { - routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, string.Empty, true)); - } - - /// - /// Clears the route on the destination. This will remove any routes that are currently in use - /// - /// destination - /// input to use to find existing route - /// - /// ClearRoute method - /// - public static void ClearRoute(this IRoutingInputs destination, string inputPortKey) - { - routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, inputPortKey, true)); - } - - /// - /// Removes the route request for the destination. This will remove any routes that are currently in use - /// - /// destination device key - public static void RemoveRouteRequestForDestination(string destinationKey) - { - Debug.LogMessage(LogEventLevel.Information, "Removing route request for {destination}", null, destinationKey); - - var result = RouteRequests.Remove(destinationKey); - - var messageTemplate = result ? "Route Request for {destination} removed" : "Route Request for {destination} not found"; - - Debug.LogMessage(LogEventLevel.Information, messageTemplate, null, destinationKey); - } - - /// - /// Builds a RouteDescriptor that contains the steps necessary to make a route between devices. - /// Routes of type AudioVideo will be built as two separate routes, audio and video. If - /// a route is discovered, a new RouteDescriptor is returned. If one or both parts - /// of an audio/video route are discovered a route descriptor is returned. If no route is - /// discovered, then null is returned - /// - public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) - { - // if it's a single signal type, find the route - if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) && - !(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio))) + var routes = singleTypeRouteDescriptor?.Routes ?? new List(); + foreach (var route in routes) { - var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType); - Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key, signalType); - - if (!destination.GetRouteToSource(source, null, null, signalType, 0, singleTypeRouteDescriptor, destinationPort, sourcePort)) - singleTypeRouteDescriptor = null; - - var routes = singleTypeRouteDescriptor?.Routes ?? new List(); - foreach (var route in routes) - { - Debug.LogMessage(LogEventLevel.Verbose, "Route for device: {route}", destination, route.ToString()); - } - - return (singleTypeRouteDescriptor, null); - } - // otherwise, audioVideo needs to be handled as two steps. - - Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination, source.Key, signalType); - - RouteDescriptor audioRouteDescriptor; - - if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio)) - { - audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio); - } - else - { - audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio); + Debug.LogMessage(LogEventLevel.Verbose, "Route for device: {route}", destination, route.ToString()); } - var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort); + return (singleTypeRouteDescriptor, null); + } + // otherwise, audioVideo needs to be handled as two steps. - if (!audioSuccess) - Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key); + Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key); - var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Video); + RouteDescriptor audioRouteDescriptor; - var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, videoRouteDescriptor, destinationPort, sourcePort); - - if (!videoSuccess) - Debug.LogMessage(LogEventLevel.Debug, "Cannot find video route to {0}", destination, source.Key); - - foreach (var route in audioRouteDescriptor.Routes) - { - Debug.LogMessage(LogEventLevel.Verbose, "Audio route for device: {route}", destination, route.ToString()); - } - - foreach (var route in videoRouteDescriptor.Routes) - { - Debug.LogMessage(LogEventLevel.Verbose, "Video route for device: {route}", destination, route.ToString()); - } - - - if (!audioSuccess && !videoSuccess) - return (null, null); - - - return (audioRouteDescriptor, videoRouteDescriptor); + if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio)) + { + audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio); + } else + { + audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio); } - /// - /// Internal method to handle the logic for releasing an existing route and making a new one. - /// Handles devices with cooling states by queueing the request. - /// - /// The destination device. - /// The source device. - /// The type of signal to route. - /// The specific destination input port (optional). - /// The specific source output port (optional). - private static void ReleaseAndMakeRoute(IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort = null, RoutingOutputPort sourcePort = null) + var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort); + + if (!audioSuccess) + Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key); + + var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Video); + + var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, videoRouteDescriptor, destinationPort, sourcePort); + + if (!videoSuccess) + Debug.LogMessage(LogEventLevel.Debug, "Cannot find video route to {0}", destination, source.Key); + + foreach (var route in audioRouteDescriptor.Routes) { - if (destination == null) throw new ArgumentNullException(nameof(destination)); - if (source == null) throw new ArgumentNullException(nameof(source)); - if (destinationPort == null) Debug.LogMessage(LogEventLevel.Information, "Destination port is null"); - if (sourcePort == null) Debug.LogMessage(LogEventLevel.Information, "Source port is null"); + Debug.LogMessage(LogEventLevel.Verbose, "Audio route for device: {route}", destination, route.ToString()); + } - var routeRequest = new RouteRequest - { - Destination = destination, - DestinationPort = destinationPort, - Source = source, - SourcePort = sourcePort, - SignalType = signalType - }; + foreach (var route in videoRouteDescriptor.Routes) + { + Debug.LogMessage(LogEventLevel.Verbose, "Video route for device: {route}", destination, route.ToString()); + } - var coolingDevice = destination as IWarmingCooling; - //We already have a route request for this device, and it's a cooling device and is cooling - if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRouteRequest) && coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) - { - coolingDevice.IsCoolingDownFeedback.OutputChange -= existingRouteRequest.HandleCooldown; + if (!audioSuccess && !videoSuccess) + return (null, null); - coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; - RouteRequests[destination.Key] = routeRequest; + return (audioRouteDescriptor, videoRouteDescriptor); + } - Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down and already has a routing request stored. Storing new route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); + /// + /// Internal method to handle the logic for releasing an existing route and making a new one. + /// Handles devices with cooling states by queueing the request. + /// + /// The destination device. + /// The source device. + /// The type of signal to route. + /// The specific destination input port (optional). + /// The specific source output port (optional). + private static void ReleaseAndMakeRoute(IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort = null, RoutingOutputPort sourcePort = null) + { + if (destination == null) throw new ArgumentNullException(nameof(destination)); + if (source == null) throw new ArgumentNullException(nameof(source)); + if (destinationPort == null) Debug.LogMessage(LogEventLevel.Information, "Destination port is null"); + if (sourcePort == null) Debug.LogMessage(LogEventLevel.Information, "Source port is null"); + var routeRequest = new RouteRequest + { + Destination = destination, + DestinationPort = destinationPort, + Source = source, + SourcePort = sourcePort, + SignalType = signalType + }; + + var coolingDevice = destination as IWarmingCooling; + + //We already have a route request for this device, and it's a cooling device and is cooling + if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRouteRequest) && coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) + { + coolingDevice.IsCoolingDownFeedback.OutputChange -= existingRouteRequest.HandleCooldown; + + coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; + + RouteRequests[destination.Key] = routeRequest; + + Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down and already has a routing request stored. Storing new route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); + + return; + } + + //New Request + if (coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) + { + coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; + + RouteRequests.Add(destination.Key, routeRequest); + + Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); + return; + } + + if (RouteRequests.ContainsKey(destination.Key) && coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == false) + { + var handledRequest = RouteRequests[destination.Key]; + + coolingDevice.IsCoolingDownFeedback.OutputChange -= handledRequest.HandleCooldown; + + RouteRequests.Remove(destination.Key); + + Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); + } + + routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination,destinationPort?.Key ?? string.Empty, false)); + + routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); + } + + /// + /// Executes the actual routing based on a . + /// Finds the route path, adds it to the collection, and executes the switches. + /// + /// The route request details. + private static void RunRouteRequest(RouteRequest request) + { + try + { + if (request.Source == null) return; - } - //New Request - if (coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) - { - coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; + var (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort); - RouteRequests.Add(destination.Key, routeRequest); - - Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); + if (audioOrSingleRoute == null && videoRoute == null) return; - } - if (RouteRequests.ContainsKey(destination.Key) && coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == false) + RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(audioOrSingleRoute); + + if (videoRoute != null) { - var handledRequest = RouteRequests[destination.Key]; - - coolingDevice.IsCoolingDownFeedback.OutputChange -= handledRequest.HandleCooldown; - - RouteRequests.Remove(destination.Key); - - Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); + RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(videoRoute); } - routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false)); + Debug.LogMessage(LogEventLevel.Verbose, "Executing full route", request.Destination); - routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); - } - - /// - /// Executes the actual routing based on a . - /// Finds the route path, adds it to the collection, and executes the switches. - /// - /// The route request details. - private static void RunRouteRequest(RouteRequest request) + audioOrSingleRoute.ExecuteRoutes(); + videoRoute?.ExecuteRoutes(); + } catch(Exception ex) { - try - { - if (request.Source == null) - return; - - var (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort); - - if (audioOrSingleRoute == null && videoRoute == null) - return; - - RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(audioOrSingleRoute); - - if (videoRoute != null) - { - RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(videoRoute); - } - - Debug.LogMessage(LogEventLevel.Verbose, "Executing full route", request.Destination); - - audioOrSingleRoute.ExecuteRoutes(); - videoRoute?.ExecuteRoutes(); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request); - } - } - - /// - /// Will release the existing route on the destination, if it is found in RouteDescriptorCollection.DefaultCollection - /// - /// - /// The input port key to use to find the route. If empty, will use the first available input port - /// If true, will clear the route on the destination. This will remove any routes that are currently in use - private static void ReleaseRouteInternal(IRoutingInputs destination, string inputPortKey, bool clearRoute) - { - try - { - Debug.LogMessage(LogEventLevel.Information, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); - - if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling) - { - var coolingDevice = destination as IWarmingCooling; - - coolingDevice.IsCoolingDownFeedback.OutputChange -= existingRequest.HandleCooldown; - } - - RouteRequests.Remove(destination.Key); - - var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPortKey); - if (current != null) - { - Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key); - current.ReleaseRoutes(clearRoute); - } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'", null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); - } - } - - /// - /// The recursive part of this. Will stop on each device, search its inputs for the - /// desired source and if not found, invoke this function for the each input port - /// hoping to find the source. - /// - /// - /// - /// The RoutingOutputPort whose link is being checked for a route - /// Prevents Devices from being twice-checked - /// This recursive function should not be called with AudioVideo - /// Just an informational counter - /// The RouteDescriptor being populated as the route is discovered - /// The RoutingOutputPort to use for the route - /// The specific source output port to use (optional) - /// true if source is hit - private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, - RoutingOutputPort outputPortToUse, List alreadyCheckedDevices, - eRoutingSignalType signalType, int cycle, RouteDescriptor routeTable, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) - { - cycle++; - - Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString()); - - RoutingInputPort goodInputPort = null; - - IEnumerable destinationTieLines; - TieLine directTie = null; - - if (destinationPort == null) - { - destinationTieLines = TieLineCollection.Default.Where(t => - t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo)); - } - else - { - destinationTieLines = TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && (t.Type.HasFlag(signalType))); - } - - // find the TieLine without a port - if (destinationPort == null && sourcePort == null) - { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key); - } - // find a tieLine to a specific destination port without a specific source port - else if (destinationPort != null && sourcePort == null) - { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key); - } - // find a tieline to a specific source port without a specific destination port - else if (destinationPort == null & sourcePort != null) - { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); - } - // find a tieline to a specific source port and destination port - else if (destinationPort != null && sourcePort != null) - { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); - } - - if (directTie != null) // Found a tie directly to the source - { - goodInputPort = directTie.DestinationPort; - } - else // no direct-connect. Walk back devices. - { - Debug.LogMessage(LogEventLevel.Verbose, "is not directly connected to {sourceKey}. Walking down tie lines", destination, source.Key); - - // No direct tie? Run back out on the inputs' attached devices... - // Only the ones that are routing devices - var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); - - //Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration - if (alreadyCheckedDevices == null) - alreadyCheckedDevices = new List(); - alreadyCheckedDevices.Add(destination as IRoutingInputsOutputs); - - foreach (var tieLine in midpointTieLines) - { - var midpointDevice = tieLine.SourcePort.ParentDevice as IRoutingInputsOutputs; - - // Check if this previous device has already been walked - if (alreadyCheckedDevices.Contains(midpointDevice)) - { - Debug.LogMessage(LogEventLevel.Verbose, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", destination, midpointDevice.Key, destination.Key); - continue; - } - - var midpointOutputPort = tieLine.SourcePort; - - Debug.LogMessage(LogEventLevel.Verbose, "Trying to find route on {midpointDeviceKey}", destination, midpointDevice.Key); - - // haven't seen this device yet. Do it. Pass the output port to the next - // level to enable switching on success - var upstreamRoutingSuccess = midpointDevice.GetRouteToSource(source, midpointOutputPort, - alreadyCheckedDevices, signalType, cycle, routeTable, null, sourcePort); - - if (upstreamRoutingSuccess) - { - Debug.LogMessage(LogEventLevel.Verbose, "Upstream device route found", destination); - Debug.LogMessage(LogEventLevel.Verbose, "Route found on {midpointDeviceKey}", destination, midpointDevice.Key); - Debug.LogMessage(LogEventLevel.Verbose, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", destination, tieLine.SourcePort, tieLine.DestinationPort); - goodInputPort = tieLine.DestinationPort; - break; // Stop looping the inputs in this cycle - } - } - } - - - if (goodInputPort == null) - { - Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key); - return false; - } - - // we have a route on corresponding inputPort. *** Do the route *** - - if (destination is IRoutingSink) - { - // it's a sink device - routeTable.Routes.Add(new RouteSwitchDescriptor(goodInputPort)); - } - else if (destination is IRouting) - { - routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort)); - } - else // device is merely IRoutingInputOutputs - Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination); - - return true; + Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request); } } + + /// + /// Will release the existing route on the destination, if it is found in RouteDescriptorCollection.DefaultCollection + /// + /// + /// The input port key to use to find the route. If empty, will use the first available input port + /// If true, will clear the route on the destination. This will remove any routes that are currently in use + private static void ReleaseRouteInternal(IRoutingInputs destination, string inputPortKey, bool clearRoute) + { + try + { + Debug.LogMessage(LogEventLevel.Information, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + + if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling) + { + var coolingDevice = destination as IWarmingCooling; + + coolingDevice.IsCoolingDownFeedback.OutputChange -= existingRequest.HandleCooldown; + } + + RouteRequests.Remove(destination.Key); + + var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPortKey); + if (current != null) + { + Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key); + current.ReleaseRoutes(clearRoute); + } + } catch (Exception ex) + { + Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'",null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + } + } + + /// + /// The recursive part of this. Will stop on each device, search its inputs for the + /// desired source and if not found, invoke this function for the each input port + /// hoping to find the source. + /// + /// + /// + /// The RoutingOutputPort whose link is being checked for a route + /// The RoutingOutputPort whose link is being checked for a route + /// The RoutingOutputPort to use for the route + /// Prevents Devices from being twice-checked + /// This recursive function should not be called with AudioVideo + /// Just an informational counter + /// The RouteDescriptor being populated as the route is discovered + /// true if source is hit + private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, + RoutingOutputPort outputPortToUse, List alreadyCheckedDevices, + eRoutingSignalType signalType, int cycle, RouteDescriptor routeTable, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) + { + cycle++; + + Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString()); + + RoutingInputPort goodInputPort = null; + + IEnumerable destinationTieLines; + TieLine directTie = null; + + if (destinationPort == null) + { + destinationTieLines = TieLineCollection.Default.Where(t => + t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo)); + } + else + { + destinationTieLines = TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && (t.Type.HasFlag(signalType))); + } + + // find the TieLine without a port + if (destinationPort == null && sourcePort == null) + { + directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key); + } + // find a tieLine to a specific destination port without a specific source port + else if (destinationPort != null && sourcePort == null) + { + directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key); + } + // find a tieline to a specific source port without a specific destination port + else if (destinationPort == null & sourcePort != null) + { + directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); + } + // find a tieline to a specific source port and destination port + else if (destinationPort != null && sourcePort != null) + { + directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); + } + + if (directTie != null) // Found a tie directly to the source + { + goodInputPort = directTie.DestinationPort; + } + else // no direct-connect. Walk back devices. + { + Debug.LogMessage(LogEventLevel.Verbose, "is not directly connected to {sourceKey}. Walking down tie lines", destination, source.Key); + + // No direct tie? Run back out on the inputs' attached devices... + // Only the ones that are routing devices + var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); + + //Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration + if (alreadyCheckedDevices == null) + alreadyCheckedDevices = new List(); + alreadyCheckedDevices.Add(destination as IRoutingInputsOutputs); + + foreach (var tieLine in midpointTieLines) + { + var midpointDevice = tieLine.SourcePort.ParentDevice as IRoutingInputsOutputs; + + // Check if this previous device has already been walked + if (alreadyCheckedDevices.Contains(midpointDevice)) + { + Debug.LogMessage(LogEventLevel.Verbose, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", destination, midpointDevice.Key, destination.Key); + continue; + } + + var midpointOutputPort = tieLine.SourcePort; + + Debug.LogMessage(LogEventLevel.Verbose, "Trying to find route on {midpointDeviceKey}", destination, midpointDevice.Key); + + // haven't seen this device yet. Do it. Pass the output port to the next + // level to enable switching on success + var upstreamRoutingSuccess = midpointDevice.GetRouteToSource(source, midpointOutputPort, + alreadyCheckedDevices, signalType, cycle, routeTable, null, sourcePort); + + if (upstreamRoutingSuccess) + { + Debug.LogMessage(LogEventLevel.Verbose, "Upstream device route found", destination); + Debug.LogMessage(LogEventLevel.Verbose, "Route found on {midpointDeviceKey}", destination, midpointDevice.Key); + Debug.LogMessage(LogEventLevel.Verbose, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", destination, tieLine.SourcePort, tieLine.DestinationPort); + goodInputPort = tieLine.DestinationPort; + break; // Stop looping the inputs in this cycle + } + } + } + + + if (goodInputPort == null) + { + Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key); + return false; + } + + // we have a route on corresponding inputPort. *** Do the route *** + + if (destination is IRoutingSink) + { + // it's a sink device + routeTable.Routes.Add(new RouteSwitchDescriptor(goodInputPort)); + } + else if (destination is IRouting) + { + routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort)); + } + else // device is merely IRoutingInputOutputs + Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination); + + return true; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IHasCurrentSourceInfoChange.cs b/src/PepperDash.Essentials.Core/Routing/IHasCurrentSourceInfoChange.cs index a9e13a07..107501b3 100644 --- a/src/PepperDash.Essentials.Core/Routing/IHasCurrentSourceInfoChange.cs +++ b/src/PepperDash.Essentials.Core/Routing/IHasCurrentSourceInfoChange.cs @@ -1,15 +1,4 @@ -/* Unmerged change from project 'PepperDash.Essentials.Core (net6)' -Before: -namespace PepperDash.Essentials.Core.Routing.Interfaces -After: -using PepperDash; -using PepperDash.Essentials; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.Routing; -using PepperDash.Essentials.Core.Routing; -using PepperDash.Essentials.Core.Routing.Interfaces -*/ -using System; +using System; namespace PepperDash.Essentials.Core { diff --git a/src/PepperDash.Essentials.Core/Routing/IInputSync.cs b/src/PepperDash.Essentials.Core/Routing/IInputSync.cs index 50f2b5fb..4898702f 100644 --- a/src/PepperDash.Essentials.Core/Routing/IInputSync.cs +++ b/src/PepperDash.Essentials.Core/Routing/IInputSync.cs @@ -1,9 +1,5 @@ using PepperDash.Core; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials.Core.Routing { diff --git a/src/PepperDash.Essentials.Core/Routing/IRmcRouting.cs b/src/PepperDash.Essentials.Core/Routing/IRmcRouting.cs index f2837350..d2752f4a 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRmcRouting.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRmcRouting.cs @@ -1,13 +1,13 @@ -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Defines the contract for IRmcRouting +/// +public interface IRmcRouting : IRoutingNumeric { /// - /// Defines the contract for IRmcRouting + /// Feedback for the current Audio/Video source as a number /// - public interface IRmcRouting : IRoutingNumeric - { - /// - /// Feedback for the current Audio/Video source as a number - /// - IntFeedback AudioVideoSourceNumericFeedback { get; } - } -} \ No newline at end of file + IntFeedback AudioVideoSourceNumericFeedback { get; } +} diff --git a/src/PepperDash.Essentials.Core/Routing/IRmcRoutingWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRmcRoutingWithFeedback.cs index ab14d066..660183c1 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRmcRoutingWithFeedback.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRmcRoutingWithFeedback.cs @@ -1,9 +1,8 @@ -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines an IRmcRouting with a feedback event +/// +public interface IRmcRoutingWithFeedback : IRmcRouting { - /// - /// Defines the contract for IRmcRoutingWithFeedback - /// - public interface IRmcRoutingWithFeedback : IRmcRouting - { - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRouting.cs b/src/PepperDash.Essentials.Core/Routing/IRouting.cs index 64bcd7a5..dd8ed041 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRouting.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRouting.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core -{ - /// - /// Defines a midpoint device as have internal routing. Any devices in the middle of the - /// signal chain, that do switching, must implement this for routing to work otherwise - /// the routing algorithm will treat the IRoutingInputsOutputs device as a passthrough - /// device. - /// - public interface IRouting : IRoutingInputsOutputs +namespace PepperDash.Essentials.Core; + +/// +/// Defines a midpoint device as have internal routing. Any devices in the middle of the +/// signal chain, that do switching, must implement this for routing to work otherwise +/// the routing algorithm will treat the IRoutingInputsOutputs device as a passthrough +/// device. +/// +public interface IRouting : IRoutingInputsOutputs { /// /// Executes a switch on the device @@ -18,10 +18,9 @@ namespace PepperDash.Essentials.Core /// output selector /// type of signal void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType); - } +} - /*public interface IRouting : IRoutingInputsOutputs - { - void ExecuteSwitch(TInputSelector inputSelector, TOutputSelector outputSelector, eRoutingSignalType signalType); - }*/ -} \ No newline at end of file +/*public interface IRouting : IRoutingInputsOutputs +{ + void ExecuteSwitch(TInputSelector inputSelector, TOutputSelector outputSelector, eRoutingSignalType signalType); +}*/ \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingFeedback.cs index 253ab8ee..a4e18e83 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingFeedback.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingFeedback.cs @@ -1,19 +1,15 @@ using System; - using PepperDash.Core; +namespace PepperDash.Essentials.Core; - -namespace PepperDash.Essentials.Core +/// +/// Defines the contract for IRoutingFeedback +/// +public interface IRoutingFeedback : IKeyName { /// - /// Defines the contract for IRoutingFeedback + /// Event raised when a numeric switch changes /// - public interface IRoutingFeedback : IKeyName - { - /// - /// Event raised when a numeric switch changes - /// - event EventHandler NumericSwitchChange; - //void OnSwitchChange(RoutingNumericEventArgs e); - } -} \ No newline at end of file + event EventHandler NumericSwitchChange; + //void OnSwitchChange(RoutingNumericEventArgs e); +} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingHasVideoInputSyncFeedbacks.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingHasVideoInputSyncFeedbacks.cs index a2b4c54e..10516aee 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingHasVideoInputSyncFeedbacks.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingHasVideoInputSyncFeedbacks.cs @@ -9,8 +9,8 @@ using Crestron.SimplSharpPro.DM; using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Defines the contract for IRoutingHasVideoInputSyncFeedbacks @@ -21,5 +21,4 @@ namespace PepperDash.Essentials.Core /// Video Input Sync Feedbacks /// FeedbackCollection VideoInputSyncFeedbacks { get; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingInputSlot.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingInputSlot.cs index f620f8bf..0137d6c1 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingInputSlot.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingInputSlot.cs @@ -1,13 +1,14 @@ -namespace PepperDash.Essentials.Core.Routing +namespace PepperDash.Essentials.Core.Routing; + + +/// +/// Defines the contract for IRoutingInputSlot +/// +public interface IRoutingInputSlot : IRoutingSlot, IOnline, IVideoSync { /// - /// Defines the contract for IRoutingInputSlot + /// Gets the Tx device key /// - public interface IRoutingInputSlot: IRoutingSlot, IOnline, IVideoSync - { - /// - /// Gets the Tx device key - /// - string TxDeviceKey { get; } - } + string TxDeviceKey { get; } } + diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingInputs.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingInputs.cs index 9543bb54..1b285e84 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingInputs.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingInputs.cs @@ -1,21 +1,14 @@ using PepperDash.Core; +namespace PepperDash.Essentials.Core; - -namespace PepperDash.Essentials.Core +/// +/// Defines the contract for IRoutingInputs +/// +public interface IRoutingInputs : IKeyed { /// - /// Defines the contract for IRoutingInputs + /// Collection of Input Ports /// - public interface IRoutingInputs : IKeyed - { - /// - /// Collection of Input Ports - /// - RoutingPortCollection InputPorts { get; } - } + RoutingPortCollection InputPorts { get; } +} -/* public interface IRoutingInputs : IKeyed - { - RoutingPortCollection, TSelector> InputPorts { get; } - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingInputsOutputs.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingInputsOutputs.cs index 89e4d031..9257b3d9 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingInputsOutputs.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingInputsOutputs.cs @@ -1,16 +1,9 @@ -namespace PepperDash.Essentials.Core -{ - /// - /// Defines the contract for IRoutingInputsOutputs - /// - public interface IRoutingInputsOutputs : IRoutingInputs, IRoutingOutputs - { - } +namespace PepperDash.Essentials.Core; + +/// +/// Defines the contract for IRoutingInputsOutputs +/// +public interface IRoutingInputsOutputs : IRoutingInputs, IRoutingOutputs +{ +} -/* /// - /// For devices like RMCs, baluns, other devices with no switching. - /// - public interface IRoutingInputsOutputs : IRoutingInputs, IRoutingOutputs - { - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingNumeric.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingNumeric.cs index cd91daa0..ba334afa 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingNumeric.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingNumeric.cs @@ -1,16 +1,16 @@ -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Defines the contract for IRoutingNumeric +/// +public interface IRoutingNumeric : IRouting { /// - /// Defines the contract for IRoutingNumeric + /// Executes a numeric switch on the device /// - public interface IRoutingNumeric : IRouting - { - /// - /// Executes a numeric switch on the device - /// - /// input selector - /// output selector - /// type of signal - void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type); - } -} \ No newline at end of file + /// input selector + /// output selector + /// type of signal + void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type); +} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingNumericWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingNumericWithFeedback.cs index 33a837a8..a59e9699 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingNumericWithFeedback.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingNumericWithFeedback.cs @@ -1,9 +1,8 @@ -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines an IRoutingNumeric with a feedback event +/// +public interface IRoutingNumericWithFeedback : IRoutingNumeric, IRoutingFeedback { - /// - /// Defines the contract for IRoutingNumericWithFeedback - /// - public interface IRoutingNumericWithFeedback : IRoutingNumeric, IRoutingFeedback - { - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingOutputSlot.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingOutputSlot.cs index 266e0724..ded827e7 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingOutputSlot.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingOutputSlot.cs @@ -1,26 +1,27 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core.Routing +namespace PepperDash.Essentials.Core.Routing; + + +/// +/// Defines the contract for IRoutingOutputSlot +/// +public interface IRoutingOutputSlot : IRoutingSlot { /// - /// Defines the contract for IRoutingOutputSlot + /// Event raised when output slot changes /// - public interface IRoutingOutputSlot : IRoutingSlot - { - /// - /// Event raised when output slot changes - /// - event EventHandler OutputSlotChanged; + event EventHandler OutputSlotChanged; - /// - /// Gets the Rx device key - /// - string RxDeviceKey { get; } + /// + /// Gets the Rx device key + /// + string RxDeviceKey { get; } - /// - /// Gets the current routes - /// - Dictionary CurrentRoutes { get; } - } + /// + /// Gets the current routes + /// + Dictionary CurrentRoutes { get; } } + diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingOutputs.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingOutputs.cs index c26e7b70..da2f6362 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingOutputs.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingOutputs.cs @@ -1,22 +1,16 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines the contract for IRoutingOutputs +/// +public interface IRoutingOutputs : IKeyed { - /// - /// Defines the contract for IRoutingOutputs + /// Collection of Output Ports /// - public interface IRoutingOutputs : IKeyed - { - /// - /// Collection of Output Ports - /// - RoutingPortCollection OutputPorts { get; } - } + RoutingPortCollection OutputPorts { get; } +} -/* public interface IRoutingOutputs : IKeyed - { - RoutingPortCollection, TSelector> OutputPorts { get; } - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs index 6e599f29..b8af02d9 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs @@ -1,29 +1,29 @@ -using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Core.Routing; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines the contract for IRoutingSink +/// +public interface IRoutingSink : IRoutingInputs, IHasCurrentSourceInfoChange +{ +} + +/// +/// For fixed-source endpoint devices with an input port +/// +public interface IRoutingSinkWithInputPort : IRoutingSink { /// - /// Defines the contract for IRoutingSink + /// Gets the current input port for this routing sink. /// - public interface IRoutingSink : IRoutingInputs, IHasCurrentSourceInfoChange - { - } + RoutingInputPort CurrentInputPort { get; } +} - /// - /// For fixed-source endpoint devices with an input port - /// - public interface IRoutingSinkWithInputPort : IRoutingSink - { - /// - /// Gets the current input port for this routing sink. - /// - RoutingInputPort CurrentInputPort { get; } - } +/// +/// Interface for routing sinks that have access to the current source information. +/// +public interface IRoutingSinkWithCurrentSources : IRoutingSink, ICurrentSources +{ +} - /// - /// Interface for routing sinks that have access to the current source information. - /// - public interface IRoutingSinkWithCurrentSources : IRoutingSink, ICurrentSources - { - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs index c0fd03df..63f51953 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs @@ -1,25 +1,12 @@ -using PepperDash.Essentials.Core.Routing; -using System; -using System.Collections.Generic; +namespace PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core + + +/// +/// Defines the contract for IRoutingSinkWithFeedback +/// +public interface IRoutingSinkWithFeedback : IRoutingSinkWithSwitching { - - /// - /// Defines the contract for IRoutingSinkWithFeedback - /// - public interface IRoutingSinkWithFeedback : IRoutingSinkWithSwitching - { - - } -/* /// - /// For fixed-source endpoint devices - /// - public interface IRoutingSinkWithFeedback : IRoutingSinkWithSwitching - { - RouteSwitchDescriptor CurrentRoute { get; } +} - event EventHandler InputChanged; - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithSwitching.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithSwitching.cs index 5c202914..88e1f72b 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithSwitching.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithSwitching.cs @@ -1,43 +1,30 @@ -using System; + +namespace PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core +/// +/// Delegate for InputChangedEventHandler +/// +public delegate void InputChangedEventHandler(IRoutingSinkWithSwitching destination, RoutingInputPort currentPort); + +/// +/// Defines the contract for IRoutingSinkWithSwitching +/// +public interface IRoutingSinkWithSwitching : IRoutingSink { /// - /// Delegate for InputChangedEventHandler + /// Executes a switch on the device /// - public delegate void InputChangedEventHandler(IRoutingSinkWithSwitching destination, RoutingInputPort currentPort); + /// input selector + void ExecuteSwitch(object inputSelector); +} +/// +/// Defines the contract for IRoutingSinkWithSwitchingWithInputPort +/// +public interface IRoutingSinkWithSwitchingWithInputPort : IRoutingSinkWithSwitching, IRoutingSinkWithInputPort +{ /// - /// Defines the contract for IRoutingSinkWithSwitching + /// Event raised when the input changes /// - public interface IRoutingSinkWithSwitching : IRoutingSink - { - /// - /// Executes a switch on the device - /// - /// input selector - void ExecuteSwitch(object inputSelector); - } - - /// - /// Defines the contract for IRoutingSinkWithSwitchingWithInputPort - /// - public interface IRoutingSinkWithSwitchingWithInputPort:IRoutingSinkWithSwitching, IRoutingSinkWithInputPort - { - /// - /// Event raised when the input changes - /// - event InputChangedEventHandler InputChanged; - } - -/* /// - /// Endpoint device like a display, that selects inputs - /// - /// - /// Defines the contract for IRoutingSinkWithSwitching - /// - public interface IRoutingSinkWithSwitching : IRoutingSink - { - void ExecuteSwitch(TSelector inputSelector); - }*/ -} \ No newline at end of file + event InputChangedEventHandler InputChanged; +} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSlot.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSlot.cs index 2e8c544f..deb5cabe 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSlot.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSlot.cs @@ -1,9 +1,4 @@ using PepperDash.Core; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials.Core.Routing { diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSource.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSource.cs index 75bf1bfb..5eaaabf8 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSource.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSource.cs @@ -1,9 +1,8 @@ -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Marker interface to identify a device that acts as the origin of a signal path (). +/// +public interface IRoutingSource : IRoutingOutputs { - /// - /// Defines the contract for IRoutingSource - /// - public interface IRoutingSource : IRoutingOutputs - { - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingWithClear.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingWithClear.cs index 2a80022d..dc027db9 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingWithClear.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingWithClear.cs @@ -1,15 +1,14 @@ -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines a routing device () that supports explicitly clearing a route on an output. +/// +public interface IRoutingWithClear : IRouting { /// - /// Defines the contract for IRoutingWithClear + /// Clears a route to an output, however a device needs to do that /// - public interface IRoutingWithClear : IRouting - { - /// - /// Clears a route to an output, however a device needs to do that - /// - /// Output to clear - /// signal type to clear - void ClearRoute(object outputSelector, eRoutingSignalType signalType); - } + /// Output to clear + /// signal type to clear + void ClearRoute(object outputSelector, eRoutingSignalType signalType); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingWithFeedback.cs index e66084a7..6578ee4f 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingWithFeedback.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingWithFeedback.cs @@ -1,30 +1,29 @@ using System.Collections.Generic; using System; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Gets a list describing the currently active routes on this device. +/// +/// The routing device where the change occurred. +/// A descriptor of the new route that was established. +/// +/// Delegate for RouteChangedEventHandler +/// +public delegate void RouteChangedEventHandler(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute); +/// +/// Defines a routing device () that provides feedback about its current routes. +/// +public interface IRoutingWithFeedback : IRouting { /// - /// Delegate for handling route change events on devices implementing . + /// Gets a list describing the currently active routes on this device. /// - /// The routing device where the change occurred. - /// A descriptor of the new route that was established. - /// - /// Delegate for RouteChangedEventHandler - /// - public delegate void RouteChangedEventHandler(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute); - /// - /// Defines a routing device () that provides feedback about its current routes. - /// - public interface IRoutingWithFeedback : IRouting - { - /// - /// Gets a list describing the currently active routes on this device. - /// - List CurrentRoutes { get; } + List CurrentRoutes { get; } - /// - /// Event triggered when a route changes on this device. - /// - event RouteChangedEventHandler RouteChanged; - } -} \ No newline at end of file + /// + /// Event triggered when a route changes on this device. + /// + event RouteChangedEventHandler RouteChanged; +} diff --git a/src/PepperDash.Essentials.Core/Routing/ITxRouting.cs b/src/PepperDash.Essentials.Core/Routing/ITxRouting.cs index 0bd818ac..67901117 100644 --- a/src/PepperDash.Essentials.Core/Routing/ITxRouting.cs +++ b/src/PepperDash.Essentials.Core/Routing/ITxRouting.cs @@ -1,17 +1,17 @@ -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a routing device (typically a transmitter or source) that provides numeric feedback for its current route. +/// Extends . +/// +public interface ITxRouting : IRoutingNumeric { /// - /// Defines the contract for ITxRouting + /// Feedback indicating the currently routed video source by its numeric identifier. /// - public interface ITxRouting : IRoutingNumeric - { - /// - /// Feedback indicating the currently routed video source by its numeric identifier. - /// - IntFeedback VideoSourceNumericFeedback { get; } - /// - /// Feedback indicating the currently routed audio source by its numeric identifier. - /// - IntFeedback AudioSourceNumericFeedback { get; } - } + IntFeedback VideoSourceNumericFeedback { get; } + /// + /// Feedback indicating the currently routed audio source by its numeric identifier. + /// + IntFeedback AudioSourceNumericFeedback { get; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/ITxRoutingWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/ITxRoutingWithFeedback.cs index abfbff7f..0ba49439 100644 --- a/src/PepperDash.Essentials.Core/Routing/ITxRoutingWithFeedback.cs +++ b/src/PepperDash.Essentials.Core/Routing/ITxRoutingWithFeedback.cs @@ -1,9 +1,8 @@ -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Defines an IRmcRouting with a feedback event +/// +public interface ITxRoutingWithFeedback : ITxRouting { - /// - /// Defines the contract for ITxRoutingWithFeedback - /// - public interface ITxRoutingWithFeedback : ITxRouting - { - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs index aab82d38..c69285dd 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs @@ -7,228 +7,208 @@ using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a collection of individual route steps between a Source and a Destination device for a specific signal type. +/// +public class RouteDescriptor { /// - /// Represents a collection of individual route steps between a Source and a Destination device for a specific signal type. + /// The destination device (sink or midpoint) for the route. /// - public class RouteDescriptor + public IRoutingInputs Destination { get; private set; } + + /// + /// The specific input port on the destination device used for this route. Can be null if not specified or applicable. + /// + public RoutingInputPort InputPort { get; private set; } + + /// + /// The source device for the route. + /// + public IRoutingOutputs Source { get; private set; } + + /// + /// The type of signal being routed (e.g., Audio, Video). This descriptor represents a single signal type. + /// + public eRoutingSignalType SignalType { get; private set; } + + /// + /// A list of individual switching steps required to establish the route. + /// + public List Routes { get; private set; } + + /// + /// Initializes a new instance of the class for a route without a specific destination input port. + /// + /// The source device. + /// The destination device. + /// The type of signal being routed. + public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) : this(source, destination, null, signalType) { - /// - /// The destination device (sink or midpoint) for the route. - /// - public IRoutingInputs Destination { get; private set; } + } - /// - /// Gets or sets the InputPort - /// - public RoutingInputPort InputPort { get; private set; } + /// + /// Initializes a new instance of the class for a route with a specific destination input port. + /// + /// The source device. + /// The destination device. + /// The destination input port (optional). + /// The signal type for this route. + public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType) + { + Destination = destination; + InputPort = inputPort; + Source = source; + SignalType = signalType; + Routes = new List(); + } - /// - /// Gets or sets the Source - /// - public IRoutingOutputs Source { get; private set; } - - /// - /// Gets or sets the SignalType - /// - public eRoutingSignalType SignalType { get; private set; } - - /// - /// Gets or sets the Routes - /// - public List Routes { get; private set; } - - /// - /// Initializes a new instance of the class for a route without a specific destination input port. - /// - /// The source device. - /// The destination device. - /// The type of signal being routed. - public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) : this(source, destination, null, signalType) + /// + /// Executes all the switching steps defined in the list. + /// + public void ExecuteRoutes() + { + foreach (var route in Routes) { - } + Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString()); - /// - /// Initializes a new instance of the class for a route with a specific destination input port. - /// - /// The source device. - /// The destination device. - /// The destination input port (optional). - /// The signal type for this route. - public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType) - { - Destination = destination; - InputPort = inputPort; - Source = source; - SignalType = signalType; - Routes = new List(); - } - - /// - /// ExecuteRoutes method - /// - public void ExecuteRoutes() - { - foreach (var route in Routes) + if (route.SwitchingDevice is IRoutingSinkWithSwitching sink) { - Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString()); - - if (route.SwitchingDevice is IRoutingSinkWithSwitching sink) - { - sink.ExecuteSwitch(route.InputPort.Selector); - continue; - } - - if (route.SwitchingDevice is IRouting switchingDevice) - { - switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); - - route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); - - Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } + sink.ExecuteSwitch(route.InputPort.Selector); + continue; } - } - /// - /// Releases the usage tracking for the route and optionally clears the route on the switching devices. - /// - /// If true, attempts to clear the route on the switching devices (e.g., set input to null/0). - - - public void ReleaseRoutes(bool clearRoute = false) - { - foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting)) + if (route.SwitchingDevice is IRouting switchingDevice) { - if (route.SwitchingDevice is IRouting switchingDevice) - { - if(clearRoute) - { - try - { - switchingDevice.ExecuteSwitch(null, route.OutputPort.Selector, SignalType); - } - catch (Exception e) - { - Debug.LogError("Error executing switch: {exception}", e.Message); - } - } + switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); - if (route.OutputPort == null) - { - continue; - } + route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); - if (route.OutputPort.InUseTracker != null) - { - route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); - Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } - else - { - Debug.LogMessage(LogEventLevel.Error, "InUseTracker is null for OutputPort {0}", null, route.OutputPort.Key); - } - } + Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); } } - - /// - /// Returns a string representation of the route descriptor, including source, destination, and individual route steps. - /// - /// A string describing the route. - - - - public override string ToString() - { - var routesText = Routes.Select(r => r.ToString()).ToArray(); - return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); - } } - /*/// - /// Represents an collection of individual route steps between Source and Destination - /// /// - /// Represents a RouteDescriptor + /// Releases the usage tracking for the route and optionally clears the route on the switching devices. /// - public class RouteDescriptor + /// If true, attempts to clear the route on the switching devices (e.g., set input to null/0). + public void ReleaseRoutes(bool clearRoute = false) { - /// - /// Gets or sets the Destination - /// - public IRoutingInputs Destination { get; private set; } - /// - /// Gets or sets the Source - /// - public IRoutingOutputs Source { get; private set; } - /// - /// Gets or sets the SignalType - /// - public eRoutingSignalType SignalType { get; private set; } - public List> Routes { get; private set; } - - - public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) + foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting)) { - Destination = destination; - Source = source; - SignalType = signalType; - Routes = new List>(); - } - - /// - /// ExecuteRoutes method - /// - public void ExecuteRoutes() - { - foreach (var route in Routes) + if (route.SwitchingDevice is IRouting switchingDevice) { - Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString()); - - if (route.SwitchingDevice is IRoutingSinkWithSwitching sink) + if(clearRoute) + { + try + { + switchingDevice.ExecuteSwitch(null, route.OutputPort.Selector, SignalType); + } + catch (Exception e) + { + Debug.LogError("Error executing switch: {exception}", e.Message); + } + } + + if (route.OutputPort == null) { - sink.ExecuteSwitch(route.InputPort.Selector); continue; } - if (route.SwitchingDevice is IRouting switchingDevice) + if (route.OutputPort.InUseTracker != null) { - switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); - - route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); - - Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } - } - } - - /// - /// ReleaseRoutes method - /// - public void ReleaseRoutes() - { - foreach (var route in Routes) - { - if (route.SwitchingDevice is IRouting) - { - // Pull the route from the port. Whatever is watching the output's in use tracker is - // responsible for responding appropriately. route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); } + else + { + Debug.LogMessage(LogEventLevel.Error, "InUseTracker is null for OutputPort {0}", null, route.OutputPort.Key); + } } } + } - /// - /// ToString method - /// - /// - public override string ToString() + /// + /// Returns a string representation of the route descriptor, including source, destination, and individual route steps. + /// + /// A string describing the route. + public override string ToString() + { + var routesText = Routes.Select(r => r.ToString()).ToArray(); + return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); + } +} + +/*/// +/// Represents an collection of individual route steps between Source and Destination +/// +public class RouteDescriptor +{ + public IRoutingInputs Destination { get; private set; } + public IRoutingOutputs Source { get; private set; } + public eRoutingSignalType SignalType { get; private set; } + public List> Routes { get; private set; } + + + public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) + { + Destination = destination; + Source = source; + SignalType = signalType; + Routes = new List>(); + } + + /// + /// Executes all routes described in this collection. Typically called via + /// extension method IRoutingInputs.ReleaseAndMakeRoute() + /// + public void ExecuteRoutes() + { + foreach (var route in Routes) { - var routesText = Routes.Select(r => r.ToString()).ToArray(); - return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); + Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString()); + + if (route.SwitchingDevice is IRoutingSinkWithSwitching sink) + { + sink.ExecuteSwitch(route.InputPort.Selector); + continue; + } + + if (route.SwitchingDevice is IRouting switchingDevice) + { + switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); + + route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); + + Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); + } } - }*/ -} \ No newline at end of file + } + + /// + /// Releases all routes in this collection. Typically called via + /// extension method IRoutingInputs.ReleaseAndMakeRoute() + /// + public void ReleaseRoutes() + { + foreach (var route in Routes) + { + if (route.SwitchingDevice is IRouting) + { + // Pull the route from the port. Whatever is watching the output's in use tracker is + // responsible for responding appropriately. + route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); + Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); + } + } + } + + public override string ToString() + { + var routesText = Routes.Select(r => r.ToString()).ToArray(); + return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); + } +}*/ \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs index 483ea22a..81f3aa6a 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs @@ -4,103 +4,90 @@ using System.Collections.Generic; using System.Linq; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// A collection of RouteDescriptors - typically the static DefaultCollection is used +/// +public class RouteDescriptorCollection { - /// - /// A collection of RouteDescriptors - typically the static DefaultCollection is used - /// - public class RouteDescriptorCollection + public static RouteDescriptorCollection DefaultCollection { - /// - /// DefaultCollection static property - /// - public static RouteDescriptorCollection DefaultCollection + get { - get - { - if (_DefaultCollection == null) - _DefaultCollection = new RouteDescriptorCollection(); - return _DefaultCollection; - } - } - private static RouteDescriptorCollection _DefaultCollection; - - private readonly List RouteDescriptors = new List(); - - /// - /// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the - /// destination exists already, it will not be added - in order to preserve - /// proper route releasing. - /// - /// - public void AddRouteDescriptor(RouteDescriptor descriptor) - { - if (descriptor == null) - { - return; - } - - if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination) - && RouteDescriptors.Any(t => t.Destination == descriptor.Destination && t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key)) - { - Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination, - "Route to [{0}] already exists in global routes table", descriptor?.Source?.Key); - return; - } - RouteDescriptors.Add(descriptor); - } - - /// - /// Gets the RouteDescriptor for a destination. Returns null if no RouteDescriptor for a destination exists. - /// - public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs destination) - { - Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}'", destination?.Key ?? null); - - return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination); - } - - /// - /// Gets the RouteDescriptor for a destination and input port key. Returns null if no matching RouteDescriptor exists. - /// - /// - /// - /// - public RouteDescriptor GetRouteDescriptorForDestinationAndInputPort(IRoutingInputs destination, string inputPortKey) - { - Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); - return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination && rd.InputPort != null && rd.InputPort.Key == inputPortKey); - } - - /// - /// Removes a RouteDescriptor from the collection based on the specified destination and input port key. - /// - /// The destination for which the route descriptor is to be removed. - /// The key of the input port associated with the route descriptor. If empty, the method will attempt to remove a descriptor based solely on the destination. - /// The removed RouteDescriptor object if a matching descriptor was found; otherwise, null. - public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination, string inputPortKey = "") - { - Debug.LogMessage(LogEventLevel.Information, "Removing route descriptor for '{destination}':'{inputPortKey}'", destination.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); - - var descr = string.IsNullOrEmpty(inputPortKey) - ? GetRouteDescriptorForDestination(destination) - : GetRouteDescriptorForDestinationAndInputPort(destination, inputPortKey); - if (descr != null) - RouteDescriptors.Remove(descr); - - Debug.LogMessage(LogEventLevel.Information, "Found route descriptor {routeDescriptor}", destination, descr); - - return descr; + if (_DefaultCollection == null) + _DefaultCollection = new RouteDescriptorCollection(); + return _DefaultCollection; } } + private static RouteDescriptorCollection _DefaultCollection; + + private readonly List RouteDescriptors = new List(); - /*/// - /// A collection of RouteDescriptors - typically the static DefaultCollection is used - /// /// - /// Represents a RouteDescriptorCollection + /// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the + /// destination exists already, it will not be added - in order to preserve + /// proper route releasing. /// - public class RouteDescriptorCollection + /// + public void AddRouteDescriptor(RouteDescriptor descriptor) + { + if (descriptor == null) + { + return; + } + + if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination) + && RouteDescriptors.Any(t => t.Destination == descriptor.Destination && t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key)) + { + Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination, + "Route to [{0}] already exists in global routes table", descriptor?.Source?.Key); + return; + } + RouteDescriptors.Add(descriptor); + } + + /// + /// Gets the RouteDescriptor for a destination + /// + /// null if no RouteDescriptor for a destination exists + public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs destination) + { + Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}'", destination?.Key ?? null); + + return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination); + } + + public RouteDescriptor GetRouteDescriptorForDestinationAndInputPort(IRoutingInputs destination, string inputPortKey) + { + Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination && rd.InputPort != null && rd.InputPort.Key == inputPortKey); + } + + /// + /// Returns the RouteDescriptor for a given destination AND removes it from collection. + /// Returns null if no route with the provided destination exists. + /// + public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination, string inputPortKey = "") + { + Debug.LogMessage(LogEventLevel.Information, "Removing route descriptor for '{destination}':'{inputPortKey}'", destination.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + + var descr = string.IsNullOrEmpty(inputPortKey) + ? GetRouteDescriptorForDestination(destination) + : GetRouteDescriptorForDestinationAndInputPort(destination, inputPortKey); + if (descr != null) + RouteDescriptors.Remove(descr); + + Debug.LogMessage(LogEventLevel.Information, "Found route descriptor {routeDescriptor}", destination, descr); + + return descr; + } +} + +/*/// +/// A collection of RouteDescriptors - typically the static DefaultCollection is used +/// +public class RouteDescriptorCollection { public static RouteDescriptorCollection DefaultCollection { @@ -158,5 +145,4 @@ namespace PepperDash.Essentials.Core RouteDescriptors.Remove(descr); return descr; } - }*/ -} \ No newline at end of file + }*/ \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteRequest.cs b/src/PepperDash.Essentials.Core/Routing/RouteRequest.cs index 30c0e5f8..08752257 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteRequest.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteRequest.cs @@ -2,77 +2,76 @@ using Serilog.Events; using System; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a request to establish a route between a source and a destination device. +/// +public class RouteRequest { /// - /// Represents a RouteRequest + /// The specific input port on the destination device to use for the route. Can be null if the port should be automatically determined or is not applicable. /// - public class RouteRequest + public RoutingInputPort DestinationPort { get; set; } + + /// + /// The specific output port on the source device to use for the route. Can be null if the port should be automatically determined or is not applicable. + /// + public RoutingOutputPort SourcePort { get; set; } + + /// + /// The destination device (sink or midpoint) for the route. + /// + public IRoutingInputs Destination { get; set; } + + /// + /// The source device for the route. + /// + public IRoutingOutputs Source { get; set; } + + /// + /// The type of signal being routed (e.g., Audio, Video, AudioVideo). + /// + public eRoutingSignalType SignalType { get; set; } + + /// + /// Handles the route request after a device's cooldown period has finished. + /// This method is typically subscribed to the IsCoolingDownFeedback.OutputChange event. + /// + /// The object that triggered the event (usually the cooling device). + /// Event arguments indicating the cooldown state change. + public void HandleCooldown(object sender, FeedbackEventArgs args) { - /// - /// The specific input port on the destination device to use for the route. Can be null if the port should be automatically determined or is not applicable. - /// - public RoutingInputPort DestinationPort { get; set; } - - /// - /// Gets or sets the SourcePort - /// - public RoutingOutputPort SourcePort { get; set; } - - /// - /// Gets or sets the Destination - /// - public IRoutingInputs Destination { get; set; } - - /// - /// Gets or sets the Source - /// - public IRoutingOutputs Source { get; set; } - - /// - /// Gets or sets the SignalType - /// - public eRoutingSignalType SignalType { get; set; } - - /// - /// Handles the route request after a device's cooldown period has finished. - /// This method is typically subscribed to the IsCoolingDownFeedback.OutputChange event. - /// - /// The object that triggered the event (usually the cooling device). - /// Event arguments indicating the cooldown state change. - public void HandleCooldown(object sender, FeedbackEventArgs args) + try { - try + Debug.LogMessage(LogEventLevel.Information, "Handling cooldown route request: {destination}:{destinationPort} -> {source}:{sourcePort} {type}", null, Destination?.Key ?? "empty destination", DestinationPort?.Key ?? "no destination port", Source?.Key ?? "empty source", SourcePort?.Key ?? "empty source port", SignalType.ToString()); + + if (args.BoolValue == true) { - Debug.LogMessage(LogEventLevel.Information, "Handling cooldown route request: {destination}:{destinationPort} -> {source}:{sourcePort} {type}", null, Destination?.Key ?? "empty destination", DestinationPort?.Key ?? "no destination port", Source?.Key ?? "empty source", SourcePort?.Key ?? "empty source port", SignalType.ToString()); - - if (args.BoolValue == true) - { - return; - } - - Debug.LogMessage(LogEventLevel.Information, "Cooldown complete. Making route from {destination} to {source}", Destination?.Key, Source?.Key); - - Destination.ReleaseAndMakeRoute(Source, SignalType, DestinationPort?.Key ?? string.Empty, SourcePort?.Key ?? string.Empty); - - if (sender is IWarmingCooling coolingDevice) - { - Debug.LogMessage(LogEventLevel.Debug, "Unsubscribing from cooling feedback for {destination}", null, Destination.Key); - coolingDevice.IsCoolingDownFeedback.OutputChange -= HandleCooldown; - } - } catch(Exception ex) - { - Debug.LogMessage(ex, "Exception handling cooldown", Destination); + return; } - } - /// - /// Returns a string representation of the route request. - /// - /// A string describing the source and destination of the route request. - public override string ToString() + Debug.LogMessage(LogEventLevel.Information, "Cooldown complete. Making route from {destination} to {source}", Destination?.Key, Source?.Key); + + Destination.ReleaseAndMakeRoute(Source, SignalType, DestinationPort?.Key ?? string.Empty, SourcePort?.Key ?? string.Empty); + + if (sender is IWarmingCooling coolingDevice) + { + Debug.LogMessage(LogEventLevel.Debug, "Unsubscribing from cooling feedback for {destination}", null, Destination.Key); + coolingDevice.IsCoolingDownFeedback.OutputChange -= HandleCooldown; + } + } catch(Exception ex) { - return $"Route {Source?.Key ?? "No Source Device"}:{SourcePort?.Key ?? "auto"} to {Destination?.Key ?? "No Destination Device"}:{DestinationPort?.Key ?? "auto"}"; + Debug.LogMessage(ex, "Exception handling cooldown", Destination); } } + + /// + /// Returns a string representation of the route request. + /// + /// A string describing the source and destination of the route request. + public override string ToString() + { + return $"Route {Source?.Key ?? "No Source Device"}:{SourcePort?.Key ?? "auto"} to {Destination?.Key ?? "No Destination Device"}:{DestinationPort?.Key ?? "auto"}"; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteRequestQueueItem.cs b/src/PepperDash.Essentials.Core/Routing/RouteRequestQueueItem.cs index 812f1399..1f7ab486 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteRequestQueueItem.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteRequestQueueItem.cs @@ -3,87 +3,86 @@ using PepperDash.Essentials.Core.Queues; using System; using Serilog.Events; -namespace PepperDash.Essentials.Core.Routing +namespace PepperDash.Essentials.Core.Routing; + +/// +/// Represents an item in the route request queue. +/// +public class RouteRequestQueueItem : IQueueMessage { /// - /// Represents a RouteRequestQueueItem + /// The action to perform for the route request. /// - public class RouteRequestQueueItem : IQueueMessage + private readonly Action action; + /// + /// The route request data. + /// + private readonly RouteRequest routeRequest; + + /// + /// Initializes a new instance of the class. + /// + /// The action to perform. + /// The route request data. + public RouteRequestQueueItem(Action routeAction, RouteRequest request) { - /// - /// The action to perform for the route request. - /// - private readonly Action action; - /// - /// The route request data. - /// - private readonly RouteRequest routeRequest; - - /// - /// Initializes a new instance of the class. - /// - /// The action to perform. - /// The route request data. - public RouteRequestQueueItem(Action routeAction, RouteRequest request) - { - action = routeAction; - routeRequest = request; - } - - /// - /// Dispatches the route request action. - /// - public void Dispatch() - { - Debug.LogMessage(LogEventLevel.Information, "Dispatching route request {routeRequest}", null, routeRequest); - action(routeRequest); - } + action = routeAction; + routeRequest = request; } /// - /// Represents a ReleaseRouteQueueItem + /// Dispatches the route request action. /// - public class ReleaseRouteQueueItem : IQueueMessage + public void Dispatch() { - /// - /// The action to perform for releasing the route. - /// - private readonly Action action; - /// - /// The destination device whose route is being released. - /// - private readonly IRoutingInputs destination; - /// - /// The specific input port key on the destination to release, or null/empty for any/default. - /// - private readonly string inputPortKey; - /// - /// Indicates whether to clear the route (send null) or just release the usage tracking. - /// - private readonly bool clearRoute; - - /// - /// Initializes a new instance of the class. - /// - /// The action to perform. - /// The destination device. - /// The input port key. - /// True to clear the route, false to just release. - public ReleaseRouteQueueItem(Action action, IRoutingInputs destination, string inputPortKey, bool clearRoute) - { - this.action = action; - this.destination = destination; - this.inputPortKey = inputPortKey; - this.clearRoute = clearRoute; - } - - /// - /// Dispatch method - /// - public void Dispatch() - { - Debug.LogMessage(LogEventLevel.Information, "Dispatching release route request for {destination}:{inputPortKey}", null, destination?.Key ?? "no destination", string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); - action(destination, inputPortKey, clearRoute); - } + Debug.LogMessage(LogEventLevel.Information, "Dispatching route request {routeRequest}", null, routeRequest); + action(routeRequest); + } +} + +/// +/// Represents an item in the queue for releasing a route. +/// +public class ReleaseRouteQueueItem : IQueueMessage +{ + /// + /// The action to perform for releasing the route. + /// + private readonly Action action; + /// + /// The destination device whose route is being released. + /// + private readonly IRoutingInputs destination; + /// + /// The specific input port key on the destination to release, or null/empty for any/default. + /// + private readonly string inputPortKey; + /// + /// Indicates whether to clear the route (send null) or just release the usage tracking. + /// + private readonly bool clearRoute; + + /// + /// Initializes a new instance of the class. + /// + /// The action to perform. + /// The destination device. + /// The input port key. + /// True to clear the route, false to just release. + public ReleaseRouteQueueItem(Action action, IRoutingInputs destination, string inputPortKey, bool clearRoute) + { + this.action = action; + this.destination = destination; + this.inputPortKey = inputPortKey; + this.clearRoute = clearRoute; + } + + /// + /// Dispatches the release route action. + /// + public void Dispatch() + { + Debug.LogMessage(LogEventLevel.Information, "Dispatching release route request for {destination}:{inputPortKey}", null, destination?.Key ?? "no destination", string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + action(destination, inputPortKey, clearRoute); } } diff --git a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs index 12aebdcf..03759018 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs @@ -1,9 +1,9 @@ -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a RouteSwitchDescriptor - /// - public class RouteSwitchDescriptor +namespace PepperDash.Essentials.Core; + +/// +/// Represents a single switching step within a larger route, detailing the switching device, input port, and optionally the output port. +/// +public class RouteSwitchDescriptor { /// /// Gets or sets the SwitchingDevice @@ -45,55 +45,10 @@ /// public override string ToString() { - if (SwitchingDevice is IRouting) - return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}"; - else - return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}"; + if (SwitchingDevice is IRouting) + return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}"; + else + return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}"; } } - /*/// - /// Represents an individual link for a route - /// - /// - /// Represents a RouteSwitchDescriptor - /// - public class RouteSwitchDescriptor - { - /// - /// Gets or sets the SwitchingDevice - /// - public IRoutingInputs SwitchingDevice { get { return InputPort.ParentDevice; } } - /// - /// Gets or sets the OutputPort - /// - public RoutingOutputPort OutputPort { get; set; } - /// - /// Gets or sets the InputPort - /// - public RoutingInputPort InputPort { get; set; } - - public RouteSwitchDescriptor(RoutingInputPort inputPort) - { - InputPort = inputPort; - } - - public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort) - { - InputPort = inputPort; - OutputPort = outputPort; - } - - /// - /// ToString method - /// - /// - public override string ToString() - { - if (SwitchingDevice is IRouting) - return string.Format("{0} switches output '{1}' to input '{2}'", SwitchingDevice.Key, OutputPort.Selector, InputPort.Selector); - else - return string.Format("{0} switches to input '{1}'", SwitchingDevice.Key, InputPort.Selector); - } - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index e37d2d57..152b245a 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -3,411 +3,307 @@ using System.Linq; using PepperDash.Core; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials.Core.Routing +namespace PepperDash.Essentials.Core.Routing; + +/// +/// Manages routing feedback by subscribing to route changes on midpoint and sink devices, +/// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. +/// +public class RoutingFeedbackManager: EssentialsDevice { /// - /// Manages routing feedback by subscribing to route changes on midpoint and sink devices, - /// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. + /// Initializes a new instance of the class. /// - public class RoutingFeedbackManager : EssentialsDevice + /// The unique key for this manager device. + /// The name of this manager device. + public RoutingFeedbackManager(string key, string name): base(key, name) + { + AddPreActivationAction(SubscribeForMidpointFeedback); + AddPreActivationAction(SubscribeForSinkFeedback); + } + + + /// + /// Subscribes to the RouteChanged event on all devices implementing . + /// + private void SubscribeForMidpointFeedback() { - /// - /// Initializes a new instance of the class. - /// - /// The unique key for this manager device. - /// The name of this manager device. - public RoutingFeedbackManager(string key, string name) - : base(key, name) + var midpointDevices = DeviceManager.AllDevices.OfType(); + + foreach (var device in midpointDevices) { - AddPreActivationAction(SubscribeForMidpointFeedback); - AddPreActivationAction(SubscribeForSinkFeedback); + device.RouteChanged += HandleMidpointUpdate; } + } - /// - /// Subscribes to the RouteChanged event on all devices implementing . - /// - private void SubscribeForMidpointFeedback() - { - var midpointDevices = DeviceManager.AllDevices.OfType(); - - foreach (var device in midpointDevices) - { - device.RouteChanged += HandleMidpointUpdate; - } - } - - /// - /// Subscribes to the InputChanged event on all devices implementing . - /// - private void SubscribeForSinkFeedback() - { - var sinkDevices = - DeviceManager.AllDevices.OfType(); + /// + /// Subscribes to the InputChanged event on all devices implementing . + /// + private void SubscribeForSinkFeedback() + { + var sinkDevices = DeviceManager.AllDevices.OfType(); foreach (var device in sinkDevices) { device.InputChanged += HandleSinkUpdate; + } + } + + /// + /// Handles the RouteChanged event from a midpoint device. + /// Triggers an update for all sink devices. + /// + /// The midpoint device that reported a route change. + /// The descriptor of the new route. + private void HandleMidpointUpdate(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute) + { + try + { + var devices = DeviceManager.AllDevices.OfType(); + + foreach (var device in devices) + { + UpdateDestination(device, device.CurrentInputPort); } } - - /// - /// Handles the RouteChanged event from a midpoint device. - /// Triggers an update for all sink devices. - /// - /// The midpoint device that reported a route change. - /// The descriptor of the new route. - private void HandleMidpointUpdate( - IRoutingWithFeedback midpoint, - RouteSwitchDescriptor newRoute - ) + catch (Exception ex) { - try - { - var devices = - DeviceManager.AllDevices.OfType(); + Debug.LogMessage(ex, "Error handling midpoint update from {midpointKey}:{Exception}", this, midpoint.Key, ex); + } + } - foreach (var device in devices) - { - UpdateDestination(device, device.CurrentInputPort); - } - } - catch (Exception ex) - { - Debug.LogMessage( - ex, - "Error handling midpoint update from {midpointKey}:{Exception}", - this, - midpoint.Key, - ex - ); - } + /// + /// Handles the InputChanged event from a sink device. + /// Triggers an update for the specific sink device. + /// + /// The sink device that reported an input change. + /// The new input port selected on the sink device. + private void HandleSinkUpdate(IRoutingSinkWithSwitching sender, RoutingInputPort currentInputPort) + { + try + { + UpdateDestination(sender, currentInputPort); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error handling Sink update from {senderKey}:{Exception}", this, sender.Key, ex); + } + } + + /// + /// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device + /// based on its currently selected input port by tracing the route back through tie lines. + /// + /// The destination sink device to update. + /// 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); + + if(inputPort == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Destination {destination} has not reported an input port yet", this,destination.Key); + return; } - /// - /// Handles the InputChanged event from a sink device. - /// Triggers an update for the specific sink device. - /// - /// The sink device that reported an input change. - /// The new input port selected on the sink device. - private void HandleSinkUpdate( - IRoutingSinkWithSwitching sender, - RoutingInputPort currentInputPort - ) + TieLine firstTieLine; + try { - try + var tieLines = TieLineCollection.Default; + + firstTieLine = tieLines.FirstOrDefault(tl => tl.DestinationPort.Key == inputPort.Key && tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key); + + if (firstTieLine == null) { - UpdateDestination(sender, currentInputPort); - } - catch (Exception ex) - { - Debug.LogMessage( - ex, - "Error handling Sink update from {senderKey}:{Exception}", - this, - sender.Key, - ex - ); - } - } - - /// - /// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device - /// based on its currently selected input port by tracing the route back through tie lines. - /// - /// The destination sink device to update. - /// The currently selected input port on the destination device. - private void UpdateDestination( - IRoutingSinkWithSwitching destination, - RoutingInputPort inputPort - ) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "Updating destination {destination} with inputPort {inputPort}", - this, - destination?.Key, - inputPort?.Key - ); - - if (inputPort == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "Destination {destination} has not reported an input port yet", - this, - destination.Key - ); - return; - } - - TieLine firstTieLine; - try - { - var tieLines = TieLineCollection.Default; - - firstTieLine = tieLines.FirstOrDefault(tl => - tl.DestinationPort.Key == inputPort.Key - && tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key - ); - - if (firstTieLine == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No tieline found for inputPort {inputPort}. Clearing current source", - this, - inputPort - ); - - var tempSourceListItem = new SourceListItem - { - SourceKey = "$transient", - Name = inputPort.Key, - }; - - destination.CurrentSourceInfo = tempSourceListItem; - ; - destination.CurrentSourceInfoKey = "$transient"; - return; - } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); - return; - } - - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Getting source for first TieLine {tieLine}", this, firstTieLine); - - TieLine sourceTieLine; - try - { - sourceTieLine = GetRootTieLine(firstTieLine); - - if (sourceTieLine == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No route found to source for inputPort {inputPort}. Clearing current source", - this, - inputPort - ); - - var tempSourceListItem = new SourceListItem - { - SourceKey = "$transient", - Name = "None", - }; - - destination.CurrentSourceInfo = tempSourceListItem; - destination.CurrentSourceInfoKey = string.Empty; - return; - } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); - return; - } - - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine); - - // Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet. - var room = DeviceManager - .AllDevices.OfType() - .FirstOrDefault( - (r) => - { - if (r is IHasMultipleDisplays roomMultipleDisplays) - { - return roomMultipleDisplays.Displays.Any(d => - d.Value.Key == destination.Key - ); - } - - if (r is IHasDefaultDisplay roomDefaultDisplay) - { - return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; - } - - return false; - } - ); - - if (room == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No room found for display {destination}", - this, - destination.Key - ); - return; - } - - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found room {room} for destination {destination}", this, room.Key, destination.Key); - - var sourceList = ConfigReader.ConfigObject.GetSourceListForKey(room.SourceListKey); - - if (sourceList == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", - this, - room.SourceListKey, - sourceTieLine - ); - return; - } - - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key); - - var sourceListItem = sourceList.FirstOrDefault(sli => - { - //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, - // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", - // this, - // sli.Key, - // sli.Value.SourceKey, - // sourceTieLine.SourcePort.ParentDevice.Key); - - return sli.Value.SourceKey.Equals( - sourceTieLine.SourcePort.ParentDevice.Key, - StringComparison.InvariantCultureIgnoreCase - ); - }); - - var source = sourceListItem.Value; - var sourceKey = sourceListItem.Key; - - if (source == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No source found for device {key}. Creating transient source for {destination}", - this, - sourceTieLine.SourcePort.ParentDevice.Key, - destination - ); + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No tieline found for inputPort {inputPort}. Clearing current source", this, inputPort); var tempSourceListItem = new SourceListItem { SourceKey = "$transient", - Name = sourceTieLine.SourcePort.Key, + Name = inputPort.Key, }; + + destination.CurrentSourceInfo = tempSourceListItem; ; destination.CurrentSourceInfoKey = "$transient"; - destination.CurrentSourceInfo = tempSourceListItem; return; } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Got Source {@source} with key {sourceKey}", this, source, sourceKey); - - destination.CurrentSourceInfoKey = sourceKey; - destination.CurrentSourceInfo = source; + } catch (Exception ex) + { + Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); + return; } - /// - /// Recursively traces a route back from a given tie line to find the root source tie line. - /// It navigates through midpoint devices () by checking their current routes. - /// - /// The starting tie line (typically connected to a sink or midpoint). - /// The connected to the original source device, or null if the source cannot be determined. - private TieLine GetRootTieLine(TieLine tieLine) + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Getting source for first TieLine {tieLine}", this, firstTieLine); + + TieLine sourceTieLine; + try { - TieLine nextTieLine = null; - try + sourceTieLine = GetRootTieLine(firstTieLine); + + if (sourceTieLine == null) { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine); + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route found to source for inputPort {inputPort}. Clearing current source", this, inputPort); - if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint) + var tempSourceListItem = new SourceListItem { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint); + SourceKey = "$transient", + Name = "None", + }; - if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "Midpoint {midpointKey} has no routes", - this, - midpoint.Key - ); - return null; - } + destination.CurrentSourceInfo = tempSourceListItem; + destination.CurrentSourceInfoKey = string.Empty; + return; + } + } catch(Exception ex) + { + Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); + return; + } - var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route => - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine); + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine); - return route.OutputPort != null - && route.InputPort != null - && route.OutputPort?.Key == tieLine.SourcePort.Key - && route.OutputPort?.ParentDevice.Key - == tieLine.SourcePort.ParentDevice.Key; - }); + // Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet. + var room = DeviceManager.AllDevices.OfType().FirstOrDefault((r) => { + if(r is IHasMultipleDisplays roomMultipleDisplays) + { + return roomMultipleDisplays.Displays.Any(d => d.Value.Key == destination.Key); + } - if (currentRoute == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No route through midpoint {midpoint} for outputPort {outputPort}", - this, - midpoint.Key, - tieLine.SourcePort - ); - return null; - } + if(r is IHasDefaultDisplay roomDefaultDisplay) + { + return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; + } - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint); + return false; + }); + + if(room == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No room found for display {destination}", this, destination.Key); + return; + } - nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key); - return tl.DestinationPort.Key == currentRoute.InputPort.Key - && tl.DestinationPort.ParentDevice.Key - == currentRoute.InputPort.ParentDevice.Key; - }); + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found room {room} for destination {destination}", this, room.Key, destination.Key); - if (nextTieLine != null) - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found next tieLine {tieLine}. Walking the chain", this, nextTieLine); - return GetRootTieLine(nextTieLine); - } + var sourceList = ConfigReader.ConfigObject.GetSourceListForKey(room.SourceListKey); - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root tieLine {tieLine}", this,nextTieLine); - return nextTieLine; + if (sourceList == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", this, room.SourceListKey, sourceTieLine); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key); + + var sourceListItem = sourceList.FirstOrDefault(sli => { + //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, + // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", + // this, + // sli.Key, + // sli.Value.SourceKey, + // sourceTieLine.SourcePort.ParentDevice.Key); + + return sli.Value.SourceKey.Equals(sourceTieLine.SourcePort.ParentDevice.Key,StringComparison.InvariantCultureIgnoreCase); + }); + + var source = sourceListItem.Value; + var sourceKey = sourceListItem.Key; + + if (source == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No source found for device {key}. Creating transient source for {destination}", this, sourceTieLine.SourcePort.ParentDevice.Key, destination); + + var tempSourceListItem = new SourceListItem + { + SourceKey = "$transient", + Name = sourceTieLine.SourcePort.Key, + }; + + destination.CurrentSourceInfoKey = "$transient"; + destination.CurrentSourceInfo = tempSourceListItem; + return; + } + + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Got Source {@source} with key {sourceKey}", this, source, sourceKey); + + destination.CurrentSourceInfoKey = sourceKey; + destination.CurrentSourceInfo = source; + + } + + /// + /// Recursively traces a route back from a given tie line to find the root source tie line. + /// It navigates through midpoint devices () by checking their current routes. + /// + /// The starting tie line (typically connected to a sink or midpoint). + /// The connected to the original source device, or null if the source cannot be determined. + private TieLine GetRootTieLine(TieLine tieLine) + { + TieLine nextTieLine = null; + try + { + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine); + + if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint) + { + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint); + + if(midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Midpoint {midpointKey} has no routes",this, midpoint.Key); + return null; } - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLIne Source Device {sourceDeviceKey} is IRoutingSource: {isIRoutingSource}", this, tieLine.SourcePort.ParentDevice.Key, tieLine.SourcePort.ParentDevice is IRoutingSource); - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source Device interfaces: {typeFullName}:{interfaces}", this, tieLine.SourcePort.ParentDevice.GetType().FullName, tieLine.SourcePort.ParentDevice.GetType().GetInterfaces().Select(i => i.Name)); + var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route => { + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine); - if ( - tieLine.SourcePort.ParentDevice is IRoutingSource - || tieLine.SourcePort.ParentDevice is IRoutingOutputs - ) //end of the chain + return route.OutputPort != null && route.InputPort != null && route.OutputPort?.Key == tieLine.SourcePort.Key && route.OutputPort?.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key; + }); + + if (currentRoute == null) { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root: {tieLine}", this, tieLine); - return tieLine; + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route through midpoint {midpoint} for outputPort {outputPort}", this, midpoint.Key, tieLine.SourcePort); + return null; } - nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => - tl.DestinationPort.Key == tieLine.SourcePort.Key - && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key - ); + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint); + + nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => { + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key); + return tl.DestinationPort.Key == currentRoute.InputPort.Key && tl.DestinationPort.ParentDevice.Key == currentRoute.InputPort.ParentDevice.Key; }); if (nextTieLine != null) { + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found next tieLine {tieLine}. Walking the chain", this, nextTieLine); return GetRootTieLine(nextTieLine); } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex); - return null; + + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root tieLine {tieLine}", this,nextTieLine); + return nextTieLine; } + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLIne Source Device {sourceDeviceKey} is IRoutingSource: {isIRoutingSource}", this, tieLine.SourcePort.ParentDevice.Key, tieLine.SourcePort.ParentDevice is IRoutingSource); + //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source Device interfaces: {typeFullName}:{interfaces}", this, tieLine.SourcePort.ParentDevice.GetType().FullName, tieLine.SourcePort.ParentDevice.GetType().GetInterfaces().Select(i => i.Name)); + + if (tieLine.SourcePort.ParentDevice is IRoutingSource || tieLine.SourcePort.ParentDevice is IRoutingOutputs) //end of the chain + { + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root: {tieLine}", this, tieLine); + return tieLine; + } + + nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => tl.DestinationPort.Key == tieLine.SourcePort.Key && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key ); + + if (nextTieLine != null) + { + return GetRootTieLine(nextTieLine); + } + } catch (Exception ex) + { + Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex); return null; } + + return null; } } diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingInputPort.cs b/src/PepperDash.Essentials.Core/Routing/RoutingInputPort.cs index 4e50f689..fbc575e9 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingInputPort.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingInputPort.cs @@ -2,18 +2,18 @@ using System; -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a RoutingInputPort - /// - public class RoutingInputPort : RoutingPort +namespace PepperDash.Essentials.Core; + +/// +/// Represents a basic routing input port on a device. +/// +public class RoutingInputPort : RoutingPort { - /// - /// The IRoutingInputs object this lives on - /// - [JsonIgnore] - public IRoutingInputs ParentDevice { get; private set; } + /// + /// The IRoutingInputs object this lives on + /// + [JsonIgnore] + public IRoutingInputs ParentDevice { get; private set; } /// /// Constructor for a basic RoutingInputPort @@ -50,49 +50,13 @@ namespace PepperDash.Essentials.Core ParentDevice = parent; } - /// - /// Returns a string representation of the input port. - /// - /// A string in the format "ParentDeviceKey|PortKey|SignalType|ConnectionType". - /// - public override string ToString() - { - return $"{ParentDevice.Key}|{Key}|{Type}|{ConnectionType}"; - } - } - - /*/// - /// Basic RoutingInput with no statuses. + /// + /// Returns a string representation of the input port. /// - public class RoutingInputPort : RoutingPort + /// A string in the format "ParentDeviceKey|PortKey|SignalType|ConnectionType". + public override string ToString() { - /// - /// Gets or sets the ParentDevice - /// - public IRoutingInputs ParentDevice { get; private set; } + return $"{ParentDevice.Key}|{Key}|{Type}|{ConnectionType}"; + } +} - /// - /// Constructor for a basic RoutingInputPort - /// - /// An object used to refer to this port in the IRouting device's ExecuteSwitch method. - /// May be string, number, whatever - /// The IRoutingInputs object this lives on - public RoutingInputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, - TSelector selector, IRoutingInputs parent) - : this(key, type, connType, selector, parent, false) - { - } - - /// - /// Constructor for a virtual routing input port that lives inside a device. For example - /// the ports that link a DM card to a DM matrix bus - /// - /// true for internal ports - public RoutingInputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, - TSelector selector, IRoutingInputs parent, bool isInternal) - : base(key, type, connType, selector, isInternal) - { - ParentDevice = parent ?? throw new ArgumentNullException(nameof(parent)); - } - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingInputPortWithVideoStatuses.cs b/src/PepperDash.Essentials.Core/Routing/RoutingInputPortWithVideoStatuses.cs index 2ecf75c4..d98c1ae3 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingInputPortWithVideoStatuses.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingInputPortWithVideoStatuses.cs @@ -1,30 +1,30 @@ -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a RoutingInputPortWithVideoStatuses - /// - public class RoutingInputPortWithVideoStatuses : RoutingInputPort - { - /// - /// Provides feedback outputs for video statuses associated with this port. - /// - public VideoStatusOutputs VideoStatus { get; private set; } +namespace PepperDash.Essentials.Core; - /// - /// Initializes a new instance of the class. - /// - /// The unique key for this port. - /// The signal type supported by this port. - /// The physical connection type of this port. - /// An object used to refer to this port in the parent device's ExecuteSwitch method. - /// The device this port belongs to. - /// A containing delegates to retrieve video status values. - public RoutingInputPortWithVideoStatuses(string key, - eRoutingSignalType type, eRoutingPortConnectionType connType, object selector, - IRoutingInputs parent, VideoStatusFuncsWrapper funcs) : - base(key, type, connType, selector, parent) - { - VideoStatus = new VideoStatusOutputs(funcs); - } +/// +/// Represents a routing input port that provides video status feedback (e.g., sync, resolution). +/// Suitable for devices like DM transmitters or DM input cards. +/// +public class RoutingInputPortWithVideoStatuses : RoutingInputPort +{ + /// + /// Provides feedback outputs for video statuses associated with this port. + /// + public VideoStatusOutputs VideoStatus { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The unique key for this port. + /// The signal type supported by this port. + /// The physical connection type of this port. + /// An object used to refer to this port in the parent device's ExecuteSwitch method. + /// The device this port belongs to. + /// A containing delegates to retrieve video status values. + public RoutingInputPortWithVideoStatuses(string key, + eRoutingSignalType type, eRoutingPortConnectionType connType, object selector, + IRoutingInputs parent, VideoStatusFuncsWrapper funcs) : + base(key, type, connType, selector, parent) + { + VideoStatus = new VideoStatusOutputs(funcs); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingNumericEventArgs.cs b/src/PepperDash.Essentials.Core/Routing/RoutingNumericEventArgs.cs index a2025dad..89cd3d5c 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingNumericEventArgs.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingNumericEventArgs.cs @@ -1,83 +1,82 @@ using System; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Provides event arguments for routing changes, potentially including numeric or port object references. +/// +public class RoutingNumericEventArgs : EventArgs { /// - /// Represents a RoutingNumericEventArgs + /// The numeric representation of the output, if applicable. /// - public class RoutingNumericEventArgs : EventArgs + public uint? Output { get; set; } + /// + /// The numeric representation of the input, if applicable. + /// + public uint? Input { get; set; } + + /// + /// The type of signal involved in the routing change. + /// + public eRoutingSignalType SigType { get; set; } + /// + /// The input port involved in the routing change, if applicable. + /// + public RoutingInputPort InputPort { get; set; } + /// + /// The output port involved in the routing change, if applicable. + /// + public RoutingOutputPort OutputPort { get; set; } + + /// + /// Initializes a new instance of the class using numeric identifiers. + /// + /// The numeric output identifier. + /// The numeric input identifier. + /// The signal type. + public RoutingNumericEventArgs(uint output, uint input, eRoutingSignalType sigType) : this(output, input, null, null, sigType) { - /// - /// The numeric representation of the output, if applicable. - /// - public uint? Output { get; set; } - /// - /// The numeric representation of the input, if applicable. - /// - public uint? Input { get; set; } + } - /// - /// The type of signal involved in the routing change. - /// - public eRoutingSignalType SigType { get; set; } - /// - /// The input port involved in the routing change, if applicable. - /// - public RoutingInputPort InputPort { get; set; } - /// - /// The output port involved in the routing change, if applicable. - /// - public RoutingOutputPort OutputPort { get; set; } + /// + /// Initializes a new instance of the class using port objects. + /// + /// The output port object. + /// The input port object. + /// The signal type. + public RoutingNumericEventArgs(RoutingOutputPort outputPort, RoutingInputPort inputPort, + eRoutingSignalType sigType) + : this(null, null, outputPort, inputPort, sigType) + { + } - /// - /// Initializes a new instance of the class using numeric identifiers. - /// - /// The numeric output identifier. - /// The numeric input identifier. - /// The signal type. - public RoutingNumericEventArgs(uint output, uint input, eRoutingSignalType sigType) : this(output, input, null, null, sigType) - { - } + /// + /// Initializes a new instance of the class with default values. + /// + public RoutingNumericEventArgs() + : this(null, null, null, null, 0) + { - /// - /// Initializes a new instance of the class using port objects. - /// - /// The output port object. - /// The input port object. - /// The signal type. - public RoutingNumericEventArgs(RoutingOutputPort outputPort, RoutingInputPort inputPort, - eRoutingSignalType sigType) - : this(null, null, outputPort, inputPort, sigType) - { - } + } - /// - /// Initializes a new instance of the class with default values. - /// - public RoutingNumericEventArgs() - : this(null, null, null, null, 0) - { - - } + /// + /// Initializes a new instance of the class with potentially mixed identifiers. + /// + /// The numeric output identifier (optional). + /// The numeric input identifier (optional). + /// The output port object (optional). + /// The input port object (optional). + /// The signal type. + public RoutingNumericEventArgs(uint? output, uint? input, RoutingOutputPort outputPort, + RoutingInputPort inputPort, eRoutingSignalType sigType) + { + OutputPort = outputPort; + InputPort = inputPort; - /// - /// Initializes a new instance of the class with potentially mixed identifiers. - /// - /// The numeric output identifier (optional). - /// The numeric input identifier (optional). - /// The output port object (optional). - /// The input port object (optional). - /// The signal type. - public RoutingNumericEventArgs(uint? output, uint? input, RoutingOutputPort outputPort, - RoutingInputPort inputPort, eRoutingSignalType sigType) - { - OutputPort = outputPort; - InputPort = inputPort; - - Output = output; - Input = input; - SigType = sigType; - } + Output = output; + Input = input; + SigType = sigType; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingOutputPort.cs b/src/PepperDash.Essentials.Core/Routing/RoutingOutputPort.cs index c1cadfb1..747106ce 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingOutputPort.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingOutputPort.cs @@ -2,105 +2,63 @@ using System; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a basic routing output port on a device. +/// +public class RoutingOutputPort : RoutingPort { /// /// Represents a RoutingOutputPort /// - public class RoutingOutputPort : RoutingPort + [JsonIgnore] + public IRoutingOutputs ParentDevice { get; private set; } + + /// + /// Tracks which destinations are currently using this output port. + /// + public InUseTracking InUseTracker { get; private set; } + + + /// + /// Initializes a new instance of the class. + /// + /// The unique key for this port. + /// The signal type supported by this port. + /// The physical connection type of this port. + /// An object used to refer to this port in the parent device's ExecuteSwitch method. + /// The device this port belongs to. + public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingOutputs parent) + : this(key, type, connType, selector, parent, false) { - /// - /// The IRoutingOutputs object this port lives on. - /// - [JsonIgnore] - public IRoutingOutputs ParentDevice { get; private set; } + } - /// - /// Tracks which destinations are currently using this output port. - /// - public InUseTracking InUseTracker { get; private set; } - - - /// - /// Initializes a new instance of the class. - /// - /// The unique key for this port. - /// The signal type supported by this port. - /// The physical connection type of this port. - /// An object used to refer to this port in the parent device's ExecuteSwitch method. - /// The device this port belongs to. - public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, - object selector, IRoutingOutputs parent) - : this(key, type, connType, selector, parent, false) - { - } - - /// - /// Initializes a new instance of the class, potentially marking it as internal. - /// - /// The unique key for this port. - /// The signal type supported by this port. - /// The physical connection type of this port. - /// An object used to refer to this port in the parent device's ExecuteSwitch method. - /// The device this port belongs to. - /// True if this port represents an internal connection within a device (e.g., card to backplane). - public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, - object selector, IRoutingOutputs parent, bool isInternal) - : base(key, type, connType, selector, isInternal) - { - ParentDevice = parent ?? throw new ArgumentNullException(nameof(parent)); - InUseTracker = new InUseTracking(); - } - - /// - /// Returns a string representation of the output port. - /// - /// A string in the format "ParentDeviceKey|PortKey|SignalType|ConnectionType". - public override string ToString() - { - return $"{ParentDevice.Key}|{Key}|{Type}|{ConnectionType}"; - } - } - - /*public class RoutingOutputPort : RoutingPort + /// + /// Initializes a new instance of the class, potentially marking it as internal. + /// + /// The unique key for this port. + /// The signal type supported by this port. + /// The physical connection type of this port. + /// An object used to refer to this port in the parent device's ExecuteSwitch method. + /// The device this port belongs to. + /// True if this port represents an internal connection within a device (e.g., card to backplane). + public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, + object selector, IRoutingOutputs parent, bool isInternal) + : base(key, type, connType, selector, isInternal) { - /// - /// Gets or sets the ParentDevice - /// - public IRoutingOutputs ParentDevice { get; private set; } + ParentDevice = parent ?? throw new ArgumentNullException(nameof(parent)); + InUseTracker = new InUseTracking(); + } - /// - /// Gets or sets the InUseTracker - /// - public InUseTracking InUseTracker { get; private set; } + /// + /// Returns a string representation of the output port. + /// + /// A string in the format "ParentDeviceKey|PortKey|SignalType|ConnectionType". + public override string ToString() + { + return $"{ParentDevice.Key}|{Key}|{Type}|{ConnectionType}"; + } +} - - /// - /// - /// An object used to refer to this port in the IRouting device's ExecuteSwitch method. - /// May be string, number, whatever - /// The IRoutingOutputs object this port lives on - public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, - TSelector selector, IRoutingOutputs parent) - : this(key, type, connType, selector, parent, false) - { - } - - public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, - TSelector selector, IRoutingOutputs parent, bool isInternal) - : base(key, type, connType, selector, isInternal) - { - ParentDevice = parent ?? throw new ArgumentNullException(nameof(parent)); - InUseTracker = new InUseTracking(); - } - - /// - /// ToString method - /// - /// - public override string ToString() - { - return ParentDevice.Key + ":" + Key; - } - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingPort.cs b/src/PepperDash.Essentials.Core/Routing/RoutingPort.cs index 30440578..7fa01811 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingPort.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingPort.cs @@ -1,12 +1,12 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ - /// - /// Base class for and . - /// - public abstract class RoutingPort : IKeyed +namespace PepperDash.Essentials.Core; + +/// +/// Base class for and . +/// +public abstract class RoutingPort : IKeyed { /// /// The unique key identifying this port within its parent device. @@ -28,14 +28,14 @@ namespace PepperDash.Essentials.Core /// Indicates if this port represents an internal connection within a device (e.g., card to backplane). /// public bool IsInternal { get; private set; } - /// - /// An object used to match feedback values to this port, if applicable. - /// - public object FeedbackMatchObject { get; set; } - /// - /// A reference to the underlying hardware port object (e.g., SimplSharpPro Port), if applicable. - /// - public object Port { get; set; } + /// + /// An object used to match feedback values to this port, if applicable. + /// + public object FeedbackMatchObject { get; set; } + /// + /// A reference to the underlying hardware port object (e.g., SimplSharpPro Port), if applicable. + /// + public object Port { get; set; } /// /// Initializes a new instance of the class. @@ -53,43 +53,5 @@ namespace PepperDash.Essentials.Core Selector = selector; IsInternal = isInternal; } - } +} - /*public abstract class RoutingPort:IKeyed - { - /// - /// Gets or sets the Key - /// - public string Key { get; private set; } - /// - /// Gets or sets the Type - /// - public eRoutingSignalType Type { get; private set; } - /// - /// Gets or sets the ConnectionType - /// - public eRoutingPortConnectionType ConnectionType { get; private set; } - public readonly TSelector Selector; - /// - /// Gets or sets the IsInternal - /// - public bool IsInternal { get; private set; } - /// - /// Gets or sets the FeedbackMatchObject - /// - public object FeedbackMatchObject { get; set; } - /// - /// Gets or sets the Port - /// - public object Port { get; set; } - - public RoutingPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, TSelector selector, bool isInternal) - { - Key = key; - Type = type; - ConnectionType = connType; - Selector = selector; - IsInternal = isInternal; - } - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs b/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs index 7ee349b5..736d507f 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Represents a RoutingPortCollection, which is essentially a List with an indexer for case-insensitive lookup of ports by their key names. /// @@ -20,21 +20,3 @@ namespace PepperDash.Essentials.Core } } } - -/* /// - /// Basically a List , with an indexer to find ports by key name - /// - public class RoutingPortCollection : List where T : RoutingPort - { - /// - /// Case-insensitive port lookup linked to ports' keys - /// - public T this[string key] - { - get - { - return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - } - } - }*/ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingPortNames.cs b/src/PepperDash.Essentials.Core/Routing/RoutingPortNames.cs index 96a2ca32..a783a0b0 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingPortNames.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingPortNames.cs @@ -4,249 +4,248 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Defines constant string values for common routing port keys. - /// These should correspond directly with the portNames var in the config tool. +/// These should correspond directly with the portNames var in the config tool. /// public class RoutingPortNames { - /// - /// antennaIn - /// + /// + /// antennaIn + /// public const string AntennaIn = "antennaIn"; - /// - /// anyAudioIn - /// + /// + /// anyAudioIn + /// public const string AnyAudioIn = "anyAudioIn"; - /// - /// anyAudioOut - /// + /// + /// anyAudioOut + /// public const string AnyAudioOut = "anyAudioOut"; - /// - /// anyOut - /// - public const string AnyOut = "anyOut"; - /// - /// anyVideoIn - /// - public const string AnyVideoIn = "anyVideoIn"; - /// - /// anyVideoOut - /// + /// + /// anyOut + /// + public const string AnyOut = "anyOut"; + /// + /// anyVideoIn + /// + public const string AnyVideoIn = "anyVideoIn"; + /// + /// anyVideoOut + /// public const string AnyVideoOut = "anyVideoOut"; - /// - /// balancedAudioOut - /// + /// + /// balancedAudioOut + /// public const string BalancedAudioOut = "balancedAudioOut"; - /// - /// codecOsd - /// - public const string CodecOsd = "codecOsd"; - /// - /// componentIn - /// + /// + /// codecOsd + /// + public const string CodecOsd = "codecOsd"; + /// + /// componentIn + /// public const string ComponentIn = "componentIn"; - /// - /// componentOut - /// + /// + /// componentOut + /// public const string ComponentOut = "componentOut"; - /// - /// compositeIn - /// + /// + /// compositeIn + /// public const string CompositeIn = "compositeIn"; - /// - /// compositeOut - /// + /// + /// compositeOut + /// public const string CompositeOut = "compositeOut"; - /// - /// displayPortIn - /// + /// + /// displayPortIn + /// public const string DisplayPortIn = "displayPortIn"; - /// - /// displayPortIn1 - /// + /// + /// displayPortIn1 + /// public const string DisplayPortIn1 = "displayPortIn1"; - /// - /// displayPortIn2 - /// + /// + /// displayPortIn2 + /// public const string DisplayPortIn2 = "displayPortIn2"; - /// - /// displayPortIn3 - /// + /// + /// displayPortIn3 + /// public const string DisplayPortIn3 = "displayPortIn3"; - /// - /// displayPortOut - /// + /// + /// displayPortOut + /// public const string DisplayPortOut = "displayPortOut"; - /// - /// dmIn - /// + /// + /// dmIn + /// public const string DmIn = "dmIn"; - /// - /// dmOut - /// + /// + /// dmOut + /// public const string DmOut = "dmOut"; - /// - /// dviIn - /// + /// + /// dviIn + /// public const string DviIn = "dviIn"; - /// - /// dviIn1 - /// - public const string DviIn1 = "dviIn1"; - /// - /// dviOut - /// - public const string DviOut = "dviOut"; - /// - /// hdmiIn - /// + /// + /// dviIn1 + /// + public const string DviIn1 = "dviIn1"; + /// + /// dviOut + /// + public const string DviOut = "dviOut"; + /// + /// hdmiIn + /// public const string HdmiIn = "hdmiIn"; - /// - /// hdmiIn1 - /// + /// + /// hdmiIn1 + /// public const string HdmiIn1 = "hdmiIn1"; - /// - /// hdmiIn1PC - /// - public const string HdmiIn1PC = "hdmiIn1PC"; - /// - /// hdmiIn2 - /// + /// + /// hdmiIn1PC + /// + public const string HdmiIn1PC = "hdmiIn1PC"; + /// + /// hdmiIn2 + /// public const string HdmiIn2 = "hdmiIn2"; - /// - /// hdmiIn2PC - /// - public const string HdmiIn2PC = "hdmiIn2PC"; - /// - /// hdmiIn3 - /// + /// + /// hdmiIn2PC + /// + public const string HdmiIn2PC = "hdmiIn2PC"; + /// + /// hdmiIn3 + /// public const string HdmiIn3 = "hdmiIn3"; - /// - /// hdmiIn4 - /// + /// + /// hdmiIn4 + /// public const string HdmiIn4 = "hdmiIn4"; - /// - /// hdmiIn5 - /// + /// + /// hdmiIn5 + /// public const string HdmiIn5 = "hdmiIn5"; - /// - /// hdmiIn6 - /// + /// + /// hdmiIn6 + /// public const string HdmiIn6 = "hdmiIn6"; - /// - /// hdmiOut - /// + /// + /// hdmiOut + /// public const string HdmiOut = "hdmiOut"; - /// - /// hdmiOut1 - /// - public const string HdmiOut1 = "hdmiOut1"; - /// - /// hdmiOut2 - /// - public const string HdmiOut2 = "hdmiOut2"; - /// - /// hdmiOut3 - /// - public const string HdmiOut3 = "hdmiOut3"; - /// - /// hdmiOut4 - /// - public const string HdmiOut4 = "hdmiOut4"; - /// - /// hdmiOut5 - /// - public const string HdmiOut5 = "hdmiOut5"; - /// - /// hdmiOut6 - /// - public const string HdmiOut6 = "hdmiOut6"; - /// - /// none - /// - public const string None = "none"; - /// - /// rgbIn - /// + /// + /// hdmiOut1 + /// + public const string HdmiOut1 = "hdmiOut1"; + /// + /// hdmiOut2 + /// + public const string HdmiOut2 = "hdmiOut2"; + /// + /// hdmiOut3 + /// + public const string HdmiOut3 = "hdmiOut3"; + /// + /// hdmiOut4 + /// + public const string HdmiOut4 = "hdmiOut4"; + /// + /// hdmiOut5 + /// + public const string HdmiOut5 = "hdmiOut5"; + /// + /// hdmiOut6 + /// + public const string HdmiOut6 = "hdmiOut6"; + /// + /// none + /// + public const string None = "none"; + /// + /// rgbIn + /// public const string RgbIn = "rgbIn"; - /// - /// rgbIn1 - /// - public const string RgbIn1 = "rgbIn1"; - /// - /// rgbIn2 - /// - public const string RgbIn2 = "rgbIn2"; - /// - /// vgaIn - /// + /// + /// rgbIn1 + /// + public const string RgbIn1 = "rgbIn1"; + /// + /// rgbIn2 + /// + public const string RgbIn2 = "rgbIn2"; + /// + /// vgaIn + /// public const string VgaIn = "vgaIn"; - /// - /// vgaIn1 - /// - public const string VgaIn1 = "vgaIn1"; - /// - /// vgaOut - /// + /// + /// vgaIn1 + /// + public const string VgaIn1 = "vgaIn1"; + /// + /// vgaOut + /// public const string VgaOut = "vgaOut"; - /// - /// IPC/OPS - /// - public const string IpcOps = "ipcOps"; - /// - /// MediaPlayer - /// - public const string MediaPlayer = "mediaPlayer"; - /// - /// UsbCIn - /// - public const string UsbCIn = "usbCIn"; - /// - /// UsbCIn1 - /// - public const string UsbCIn1 = "usbCIn1"; - /// - /// UsbCIn2 - /// - public const string UsbCIn2 = "usbCIn2"; - /// - /// UsbCIn3 - /// - public const string UsbCIn3 = "usbCIn3"; - /// - /// UsbCOut - /// - public const string UsbCOut = "usbCOut"; - /// - /// UsbCOut1 - /// - public const string UsbCOut1 = "usbCOut1"; - /// - /// UsbCOut2 - /// - public const string UsbCOut2 = "usbCOut2"; - /// - /// UsbCOut3 - /// - public const string UsbCOut3 = "usbCOut3"; - /// - /// HdBaseTIn - /// - public const string HdBaseTIn = "hdBaseTIn"; - /// - /// HdBaseTOut - /// - public const string HdBaseTOut = "hdBaseTOut"; - /// - /// SdiIn - /// - public const string SdiIn = "sdiIn"; - /// - /// SdiOut - /// - public const string SdiOut = "sdiOut"; - } + /// + /// IPC/OPS + /// + public const string IpcOps = "ipcOps"; + /// + /// MediaPlayer + /// + public const string MediaPlayer = "mediaPlayer"; + /// + /// UsbCIn + /// + public const string UsbCIn = "usbCIn"; + /// + /// UsbCIn1 + /// + public const string UsbCIn1 = "usbCIn1"; + /// + /// UsbCIn2 + /// + public const string UsbCIn2 = "usbCIn2"; + /// + /// UsbCIn3 + /// + public const string UsbCIn3 = "usbCIn3"; + /// + /// UsbCOut + /// + public const string UsbCOut = "usbCOut"; + /// + /// UsbCOut1 + /// + public const string UsbCOut1 = "usbCOut1"; + /// + /// UsbCOut2 + /// + public const string UsbCOut2 = "usbCOut2"; + /// + /// UsbCOut3 + /// + public const string UsbCOut3 = "usbCOut3"; + /// + /// HdBaseTIn + /// + public const string HdBaseTIn = "hdBaseTIn"; + /// + /// HdBaseTOut + /// + public const string HdBaseTOut = "hdBaseTOut"; + /// + /// SdiIn + /// + public const string SdiIn = "sdiIn"; + /// + /// SdiOut + /// + public const string SdiOut = "sdiOut"; } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/TieLine.cs b/src/PepperDash.Essentials.Core/Routing/TieLine.cs index 42f024b4..8b2f8863 100644 --- a/src/PepperDash.Essentials.Core/Routing/TieLine.cs +++ b/src/PepperDash.Essentials.Core/Routing/TieLine.cs @@ -2,154 +2,152 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Represents a connection (tie line) between a and a . +/// +public class TieLine { /// /// Represents a connection between routing ports, linking a source output port to a destination input port. /// This class is used to define signal paths for routing algorithms, including signal type overrides and internal connections. /// - public class TieLine - { - /// - /// The source output port of the tie line. - /// - public RoutingOutputPort SourcePort { get; private set; } - /// - /// The destination input port of the tie line. - /// - public RoutingInputPort DestinationPort { get; private set; } - //public int InUseCount { get { return DestinationUsingThis.Count; } } - - /// - /// Gets the type of this tie line. Will either be the type of the destination port - /// or the type of OverrideType when it is set. - /// - public eRoutingSignalType Type - { - get - { - if (OverrideType.HasValue) return OverrideType.Value; - return DestinationPort.Type; - } - } - - /// - /// Use this to override the Type property for the destination port. For example, - /// when the tie line is type AudioVideo, and the signal flow should be limited to - /// Audio-only or Video only, changing this type will alter the signal paths - /// available to the routing algorithm without affecting the actual Type - /// of the destination port. - /// - public eRoutingSignalType? OverrideType { get; set; } - - //List DestinationUsingThis = new List(); - - /// - /// Gets a value indicating whether this tie line represents an internal connection within a device (both source and destination ports are internal). - /// - public bool IsInternal { get { return SourcePort.IsInternal && DestinationPort.IsInternal; } } - /// - /// Gets a value indicating whether the signal types of the source and destination ports differ. - /// - public bool TypeMismatch { get { return SourcePort.Type != DestinationPort.Type; } } - /// - /// Gets a value indicating whether the connection types of the source and destination ports differ. - /// - public bool ConnectionTypeMismatch { get { return SourcePort.ConnectionType != DestinationPort.ConnectionType; } } - /// - /// A descriptive note about any type mismatch, if applicable. - /// - public string TypeMismatchNote { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The source output port. - /// The destination input port. - public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort) - { - if (sourcePort == null || destinationPort == null) - throw new ArgumentNullException("source or destination port"); - SourcePort = sourcePort; - DestinationPort = destinationPort; - } - - /// - /// Creates a tie line with an overriding Type. See help for OverrideType property for info. - /// - /// The source output port. - /// The destination input port. - /// The signal type to limit the link to. Overrides DestinationPort.Type for routing calculations. - public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType? overrideType) : - this(sourcePort, destinationPort) - { - OverrideType = overrideType; - } - - /// - /// Creates a tie line with an overriding Type. See help for OverrideType property for info. - /// - /// The source output port. - /// The destination input port. - /// The signal type to limit the link to. Overrides DestinationPort.Type for routing calculations. - public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType overrideType) : - this(sourcePort, destinationPort) - { - OverrideType = overrideType; - } - - /// - /// Will link up video status from supporting inputs to connected outputs. - /// - public void Activate() - { - // Now does nothing - } - - /// - /// Deactivates the tie line. - /// - public void Deactivate() - { - // Now does nothing - } - - /// - /// Returns a string representation of the tie line. - /// - /// A string describing the source, destination, and type of the tie line. - public override string ToString() - { - return string.Format("Tie line: {0}:{1} --> {2}:{3} {4}", SourcePort.ParentDevice.Key, SourcePort.Key, - DestinationPort.ParentDevice.Key, DestinationPort.Key, Type.ToString()); - } - } - - //******************************************************************************** + public RoutingOutputPort SourcePort { get; private set; } + /// + /// The destination input port of the tie line. + /// + public RoutingInputPort DestinationPort { get; private set; } + //public int InUseCount { get { return DestinationUsingThis.Count; } } /// /// Represents a collection of objects, which define signal paths for routing algorithms. /// This class provides functionality for managing tie lines and includes a singleton instance for global access. /// - public class TieLineCollection : List + public eRoutingSignalType Type { - /// - /// Gets the default singleton instance of the . - /// - public static TieLineCollection Default + get { - get - { - if (_Default == null) - _Default = new TieLineCollection(); - return _Default; - } + if (OverrideType.HasValue) return OverrideType.Value; + return DestinationPort.Type; } - - /// - /// Backing field for the singleton instance. - /// - [JsonIgnore] - private static TieLineCollection _Default; } + + /// + /// Use this to override the Type property for the destination port. For example, + /// when the tie line is type AudioVideo, and the signal flow should be limited to + /// Audio-only or Video only, changing this type will alter the signal paths + /// available to the routing algorithm without affecting the actual Type + /// of the destination port. + /// + public eRoutingSignalType? OverrideType { get; set; } + + //List DestinationUsingThis = new List(); + + /// + /// Gets a value indicating whether this tie line represents an internal connection within a device (both source and destination ports are internal). + /// + public bool IsInternal { get { return SourcePort.IsInternal && DestinationPort.IsInternal; } } + /// + /// Gets a value indicating whether the signal types of the source and destination ports differ. + /// + public bool TypeMismatch { get { return SourcePort.Type != DestinationPort.Type; } } + /// + /// Gets a value indicating whether the connection types of the source and destination ports differ. + /// + public bool ConnectionTypeMismatch { get { return SourcePort.ConnectionType != DestinationPort.ConnectionType; } } + /// + /// A descriptive note about any type mismatch, if applicable. + /// + public string TypeMismatchNote { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The source output port. + /// The destination input port. + public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort) + { + if (sourcePort == null || destinationPort == null) + throw new ArgumentNullException("source or destination port"); + SourcePort = sourcePort; + DestinationPort = destinationPort; + } + + /// + /// Creates a tie line with an overriding Type. See help for OverrideType property for info. + /// + /// The source output port. + /// The destination input port. + /// The signal type to limit the link to. Overrides DestinationPort.Type for routing calculations. + public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType? overrideType) : + this(sourcePort, destinationPort) + { + OverrideType = overrideType; + } + + /// + /// Creates a tie line with an overriding Type. See help for OverrideType property for info. + /// + /// The source output port. + /// The destination input port. + /// The signal type to limit the link to. Overrides DestinationPort.Type for routing calculations. + public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType overrideType) : + this(sourcePort, destinationPort) + { + OverrideType = overrideType; + } + + /// + /// Will link up video status from supporting inputs to connected outputs. + /// + public void Activate() + { + // Now does nothing + } + + /// + /// Deactivates the tie line. + /// + public void Deactivate() + { + // Now does nothing + } + + /// + /// Returns a string representation of the tie line. + /// + /// A string describing the source, destination, and type of the tie line. + public override string ToString() + { + return string.Format("Tie line: {0}:{1} --> {2}:{3} {4}", SourcePort.ParentDevice.Key, SourcePort.Key, + DestinationPort.ParentDevice.Key, DestinationPort.Key, Type.ToString()); + } +} + +//******************************************************************************** + +/// +/// Represents a collection of objects. +/// +public class TieLineCollection : List +{ + /// + /// Gets the default singleton instance of the . + /// + public static TieLineCollection Default + { + get + { + if (_Default == null) + _Default = new TieLineCollection(); + return _Default; + } + } + + /// + /// Backing field for the singleton instance. + /// + [JsonIgnore] + private static TieLineCollection _Default; } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/TieLineConfig.cs b/src/PepperDash.Essentials.Core/Routing/TieLineConfig.cs index 156b39b1..3ea456d3 100644 --- a/src/PepperDash.Essentials.Core/Routing/TieLineConfig.cs +++ b/src/PepperDash.Essentials.Core/Routing/TieLineConfig.cs @@ -11,8 +11,8 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.Config -{ +namespace PepperDash.Essentials.Core.Config; + /// /// Represents the configuration data for a single tie line between two routing ports. /// @@ -116,5 +116,4 @@ namespace PepperDash.Essentials.Core.Config { return $"{SourceKey}.{SourceCard}.{SourcePort} --> {DestinationKey}.{DestinationCard}.{DestinationPort}"; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/eRoutingPortConnectionType.cs b/src/PepperDash.Essentials.Core/Routing/eRoutingPortConnectionType.cs index 9245dd48..33332856 100644 --- a/src/PepperDash.Essentials.Core/Routing/eRoutingPortConnectionType.cs +++ b/src/PepperDash.Essentials.Core/Routing/eRoutingPortConnectionType.cs @@ -100,4 +100,4 @@ /// HdBaseT } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs b/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs index 9fce9c5b..e3d679da 100644 --- a/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs +++ b/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs @@ -39,4 +39,4 @@ namespace PepperDash.Essentials.Core /// SecondaryAudio = 32 } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Core/Secrets/CrestronGlobalSecretsProvider.cs b/src/PepperDash.Essentials.Core/Secrets/CrestronGlobalSecretsProvider.cs index 45983b0c..d89efd2c 100644 --- a/src/PepperDash.Essentials.Core/Secrets/CrestronGlobalSecretsProvider.cs +++ b/src/PepperDash.Essentials.Core/Secrets/CrestronGlobalSecretsProvider.cs @@ -5,115 +5,97 @@ using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public class CrestronGlobalSecretsProvider : ISecretProvider { - /// - /// Represents a CrestronGlobalSecretsProvider - /// - public class CrestronGlobalSecretsProvider : ISecretProvider + public string Key { get; set; } + //Added for reference + public string Description { get; private set; } + + public CrestronGlobalSecretsProvider(string key) { - /// - /// Gets or sets the Key - /// - public string Key { get; set; } - //Added for reference - /// - /// Gets or sets the Description - /// - public string Description { get; private set; } + Key = key; + Description = String.Format("Default secret provider serving all local applications"); - /// - /// Constructor for CrestronGlobalSecretsProvider - /// - /// The key for the secret provider - public CrestronGlobalSecretsProvider(string key) + } + + static CrestronGlobalSecretsProvider() + { + //Added for future encrypted reference + var secureSupported = CrestronSecureStorage.Supported; + + CrestronDataStoreStatic.InitCrestronDataStore(); + if (secureSupported) { - Key = key; - Description = String.Format("Default secret provider serving all local applications"); - - } - - static CrestronGlobalSecretsProvider() - { - //Added for future encrypted reference - var secureSupported = CrestronSecureStorage.Supported; - - CrestronDataStoreStatic.InitCrestronDataStore(); - if (secureSupported) - { - //doThingsFuture - } - } - - /// - /// Set secret for item in the CrestronSecretsProvider - /// - /// Secret Key - /// Secret Value - public bool SetSecret(string key, object value) - { - var secret = value as string; - CrestronDataStore.CDS_ERROR returnCode; - - if (String.IsNullOrEmpty(secret)) - { - returnCode = CrestronDataStoreStatic.clearGlobal(key); - if (returnCode == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - { - Debug.LogMessage(LogEventLevel.Information, this, "Successfully removed secret \"{0}\"", secret); - return true; - } - } - - else - { - returnCode = CrestronDataStoreStatic.SetGlobalStringValue(key, secret); - if (returnCode == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - { - Debug.LogMessage(LogEventLevel.Information, this, "Successfully set secret \"{0}\"", secret); - return true; - } - } - - Debug.LogMessage(LogEventLevel.Information, this, "Unable to set secret for {0}:{1} - {2}", Key, key, returnCode.ToString()); - return false; - } - - /// - /// Retrieve secret for item in the CrestronSecretsProvider - /// - /// Secret Key - /// ISecret Object containing key, provider, and value - - - - public ISecret GetSecret(string key) - { - string mySecret; - var getErrorCode = CrestronDataStoreStatic.GetGlobalStringValue(key, out mySecret); - - switch (getErrorCode) - { - case CrestronDataStore.CDS_ERROR.CDS_SUCCESS: - Debug.LogMessage(LogEventLevel.Verbose, this, "Secret Successfully retrieved for {0}:{1}", Key, key); - return new CrestronSecret(key, mySecret, this); - default: - Debug.LogMessage(LogEventLevel.Information, this, "Unable to retrieve secret for {0}:{1} - {2}", - Key, key, getErrorCode.ToString()); - return null; - } - } - - /// - /// Determine if a secret is present within the provider without retrieving it - /// - /// Secret Key - /// bool if present - public bool TestSecret(string key) - { - string mySecret; - return CrestronDataStoreStatic.GetGlobalStringValue(key, out mySecret) == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + //doThingsFuture } } + /// + /// Set secret for item in the CrestronSecretsProvider + /// + /// Secret Key + /// Secret Value + public bool SetSecret(string key, object value) + { + var secret = value as string; + CrestronDataStore.CDS_ERROR returnCode; + + if (String.IsNullOrEmpty(secret)) + { + returnCode = CrestronDataStoreStatic.clearGlobal(key); + if (returnCode == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + { + Debug.LogMessage(LogEventLevel.Information, this, "Successfully removed secret \"{0}\"", secret); + return true; + } + } + + else + { + returnCode = CrestronDataStoreStatic.SetGlobalStringValue(key, secret); + if (returnCode == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + { + Debug.LogMessage(LogEventLevel.Information, this, "Successfully set secret \"{0}\"", secret); + return true; + } + } + + Debug.LogMessage(LogEventLevel.Information, this, "Unable to set secret for {0}:{1} - {2}", Key, key, returnCode.ToString()); + return false; + } + + /// + /// Retrieve secret for item in the CrestronSecretsProvider + /// + /// Secret Key + /// ISecret Object containing key, provider, and value + public ISecret GetSecret(string key) + { + string mySecret; + var getErrorCode = CrestronDataStoreStatic.GetGlobalStringValue(key, out mySecret); + + switch (getErrorCode) + { + case CrestronDataStore.CDS_ERROR.CDS_SUCCESS: + Debug.LogMessage(LogEventLevel.Verbose, this, "Secret Successfully retrieved for {0}:{1}", Key, key); + return new CrestronSecret(key, mySecret, this); + default: + Debug.LogMessage(LogEventLevel.Information, this, "Unable to retrieve secret for {0}:{1} - {2}", + Key, key, getErrorCode.ToString()); + return null; + } + } + + /// + /// Determine if a secret is present within the provider without retrieving it + /// + /// Secret Key + /// bool if present + public bool TestSecret(string key) + { + string mySecret; + return CrestronDataStoreStatic.GetGlobalStringValue(key, out mySecret) == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Secrets/CrestronLocalSecretsProvider.cs b/src/PepperDash.Essentials.Core/Secrets/CrestronLocalSecretsProvider.cs index 31a1faa3..0bffd23e 100644 --- a/src/PepperDash.Essentials.Core/Secrets/CrestronLocalSecretsProvider.cs +++ b/src/PepperDash.Essentials.Core/Secrets/CrestronLocalSecretsProvider.cs @@ -6,117 +6,97 @@ using Crestron.SimplSharpPro; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public class CrestronLocalSecretsProvider : ISecretProvider { - /// - /// Represents a CrestronLocalSecretsProvider - /// - public class CrestronLocalSecretsProvider : ISecretProvider + public string Key { get; set; } + //Added for reference + public string Description { get; private set; } + + + public CrestronLocalSecretsProvider(string key) { - /// - /// Gets or sets the Key - /// - public string Key { get; set; } - //Added for reference - /// - /// Gets or sets the Description - /// - public string Description { get; private set; } + Key = key; + Description = String.Format("Default secret provider serving Essentials Application {0}", InitialParametersClass.ApplicationNumber); + } - /// - /// Constructor for CrestronLocalSecretsProvider - /// - /// The key for the secret provider - public CrestronLocalSecretsProvider(string key) + static CrestronLocalSecretsProvider() + { + //Added for future encrypted reference + var secureSupported = CrestronSecureStorage.Supported; + + CrestronDataStoreStatic.InitCrestronDataStore(); + if (secureSupported) { - Key = key; - Description = String.Format("Default secret provider serving Essentials Application {0}", InitialParametersClass.ApplicationNumber); - } - - static CrestronLocalSecretsProvider() - { - //Added for future encrypted reference - var secureSupported = CrestronSecureStorage.Supported; - - CrestronDataStoreStatic.InitCrestronDataStore(); - if (secureSupported) - { - //doThingsFuture - } - } - - /// - /// Set secret for item in the CrestronSecretsProvider - /// - /// Secret Key - /// Secret Value - public bool SetSecret(string key, object value) - { - var secret = value as string; - CrestronDataStore.CDS_ERROR returnCode; - - if (String.IsNullOrEmpty(secret)) - { - returnCode = CrestronDataStoreStatic.clearLocal(key); - if (returnCode == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - { - Debug.LogMessage(LogEventLevel.Information, this, "Successfully removed secret \"{0}\"", secret); - return true; - } - } - - else - { - returnCode = CrestronDataStoreStatic.SetLocalStringValue(key, secret); - if (returnCode == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - { - Debug.LogMessage(LogEventLevel.Information, this, "Successfully set secret \"{0}\"", secret); - return true; - } - } - - Debug.LogMessage(LogEventLevel.Information, this, "Unable to set secret for {0}:{1} - {2}", Key, key, returnCode.ToString()); - return false; - } - - /// - /// Retrieve secret for item in the CrestronSecretsProvider - /// - /// Secret Key - /// ISecret Object containing key, provider, and value - /// - /// GetSecret method - /// - public ISecret GetSecret(string key) - { - string mySecret; - var getErrorCode = CrestronDataStoreStatic.GetLocalStringValue(key, out mySecret); - - switch (getErrorCode) - { - case CrestronDataStore.CDS_ERROR.CDS_SUCCESS: - Debug.LogMessage(LogEventLevel.Verbose, this, "Secret Successfully retrieved for {0}:{1}", Key, key); - return new CrestronSecret(key, mySecret, this); - default: - Debug.LogMessage(LogEventLevel.Information, this, "Unable to retrieve secret for {0}:{1} - {2}", - Key, key, getErrorCode.ToString()); - return null; - } - } - - /// - /// Determine if a secret is present within the provider without retrieving it - /// - /// Secret Key - /// bool if present - /// - /// TestSecret method - /// - public bool TestSecret(string key) - { - string mySecret; - return CrestronDataStoreStatic.GetLocalStringValue(key, out mySecret) == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + //doThingsFuture } } + /// + /// Set secret for item in the CrestronSecretsProvider + /// + /// Secret Key + /// Secret Value + public bool SetSecret(string key, object value) + { + var secret = value as string; + CrestronDataStore.CDS_ERROR returnCode; + + if (String.IsNullOrEmpty(secret)) + { + returnCode = CrestronDataStoreStatic.clearLocal(key); + if (returnCode == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + { + Debug.LogMessage(LogEventLevel.Information, this, "Successfully removed secret \"{0}\"", secret); + return true; + } + } + + else + { + returnCode = CrestronDataStoreStatic.SetLocalStringValue(key, secret); + if (returnCode == CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + { + Debug.LogMessage(LogEventLevel.Information, this, "Successfully set secret \"{0}\"", secret); + return true; + } + } + + Debug.LogMessage(LogEventLevel.Information, this, "Unable to set secret for {0}:{1} - {2}", Key, key, returnCode.ToString()); + return false; + } + + /// + /// Retrieve secret for item in the CrestronSecretsProvider + /// + /// Secret Key + /// ISecret Object containing key, provider, and value + public ISecret GetSecret(string key) + { + string mySecret; + var getErrorCode = CrestronDataStoreStatic.GetLocalStringValue(key, out mySecret); + + switch (getErrorCode) + { + case CrestronDataStore.CDS_ERROR.CDS_SUCCESS: + Debug.LogMessage(LogEventLevel.Verbose, this, "Secret Successfully retrieved for {0}:{1}", Key, key); + return new CrestronSecret(key, mySecret, this); + default: + Debug.LogMessage(LogEventLevel.Information, this, "Unable to retrieve secret for {0}:{1} - {2}", + Key, key, getErrorCode.ToString()); + return null; + } + } + + /// + /// Determine if a secret is present within the provider without retrieving it + /// + /// Secret Key + /// bool if present + public bool TestSecret(string key) + { + string mySecret; + return CrestronDataStoreStatic.GetLocalStringValue(key, out mySecret) == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Secrets/CrestronSecret.cs b/src/PepperDash.Essentials.Core/Secrets/CrestronSecret.cs index 5a606453..1b628a44 100644 --- a/src/PepperDash.Essentials.Core/Secrets/CrestronSecret.cs +++ b/src/PepperDash.Essentials.Core/Secrets/CrestronSecret.cs @@ -4,41 +4,23 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// Special container class for CrestronSecret provider +/// +public class CrestronSecret : ISecret { - /// - /// Special container class for CrestronSecret provider - /// - public class CrestronSecret : ISecret + public ISecretProvider Provider { get; private set; } + public string Key { get; private set; } + + public object Value { get; private set; } + + public CrestronSecret(string key, string value, ISecretProvider provider) { - /// - /// Gets the Provider - /// - public ISecretProvider Provider { get; private set; } - - /// - /// Gets the Key - /// - public string Key { get; private set; } - - /// - /// Gets the Value - /// - public object Value { get; private set; } - - /// - /// Constructor for CrestronSecret - /// - /// key for the secret - /// value of the secret - /// provider of the secret - public CrestronSecret(string key, string value, ISecretProvider provider) - { - Key = key; - Value = value; - Provider = provider; - } - + Key = key; + Value = value; + Provider = provider; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Secrets/Interfaces.cs b/src/PepperDash.Essentials.Core/Secrets/Interfaces.cs index 55ce5ccc..f0b7fd38 100644 --- a/src/PepperDash.Essentials.Core/Secrets/Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Secrets/Interfaces.cs @@ -1,58 +1,57 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +/// +/// All ISecrecretProvider classes must implement this interface. +/// +public interface ISecretProvider : IKeyed { /// - /// Defines the contract for ISecretProvider + /// Set secret value for provider by key /// - public interface ISecretProvider : IKeyed - { - /// - /// Set secret value for provider by key - /// - /// key of secret to set - /// value to set secret to - /// - bool SetSecret(string key, object value); - - /// - /// Return object containing secret from provider - /// - /// key of secret to retrieve - /// - ISecret GetSecret(string key); - - /// - /// Verifies presence of secret - /// - /// key of secret to chek - /// - bool TestSecret(string key); - - /// - /// Description of the secrets provider - /// - string Description { get; } - } + /// key of secret to set + /// value to set secret to + /// + bool SetSecret(string key, object value); /// - /// interface for delivering secrets in Essentials. + /// Return object containing secret from provider /// - public interface ISecret - { - /// - /// Instance of ISecretProvider that the secret belongs to - /// - ISecretProvider Provider { get; } + /// key of secret to retrieve + /// + ISecret GetSecret(string key); - /// - /// Key of the secret in the provider - /// - string Key { get; } + /// + /// Verifies presence of secret + /// + /// key of secret to chek + /// + bool TestSecret(string key); - /// - /// Value of the secret - /// - object Value { get; } - } + /// + /// Description of the secrets provider + /// + string Description { get; } +} + +/// +/// interface for delivering secrets in Essentials. +/// +public interface ISecret +{ + /// + /// Instance of ISecretProvider that the secret belongs to + /// + ISecretProvider Provider { get; } + + /// + /// Key of the secret in the provider + /// + string Key { get; } + + /// + /// Value of the secret + /// + object Value { get; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs b/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs index 382f5d55..f35866fd 100644 --- a/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs +++ b/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs @@ -5,24 +5,19 @@ using Crestron.SimplSharp; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public static class SecretsManager { + public static Dictionary Secrets { get; private set; } + /// - /// SecretsManager static class + /// Initialize the SecretsManager /// - public static class SecretsManager + public static void Initialize() { - /// - /// Gets the Secrets dictionary - /// - public static Dictionary Secrets { get; private set; } - - /// - /// Initialize method - /// - public static void Initialize() + try { - AddSecretProvider("default", new CrestronLocalSecretsProvider("default")); AddSecretProvider("CrestronGlobalSecrets", new CrestronGlobalSecretsProvider("CrestronGlobalSecrets")); @@ -47,351 +42,338 @@ namespace PepperDash.Essentials.Core "Return data about secrets provider", ConsoleAccessLevelEnum.AccessAdministrator); } - - static SecretsManager() + catch (Exception e) { - Secrets = new Dictionary(); - } - - /// - /// Get Secret Provider from dictionary by key - /// - /// Dictionary Key for provider - /// ISecretProvider - /// - /// GetSecretProviderByKey method - /// - public static ISecretProvider GetSecretProviderByKey(string key) - { - ISecretProvider secret; - - Secrets.TryGetValue(key, out secret); - - if (secret == null) - { - Debug.LogMessage(LogEventLevel.Debug, "SecretsManager unable to retrieve SecretProvider with the key '{0}'", key); - } - return secret; - } - - /// - /// GetProviderInfo method - /// - public static void GetProviderInfo(string cmd) - { - string response; - var args = cmd.Split(' '); - - if (cmd.Length == 0 || (args.Length == 1 && args[0] == "?")) - { - response = "Returns data about secrets provider. Format 'secretproviderinfo '"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - if (args.Length == 1) - { - var provider = GetSecretProviderByKey(args[0]); - - if (provider == null) - { - response = "Invalid secrets provider key"; - CrestronConsole.ConsoleCommandResponse(response); - return; - } - - response = String.Format("{0} : {1}", provider.Key, provider.Description); - CrestronConsole.ConsoleCommandResponse(response); - return; - } - - response = "Improper number of arguments"; - CrestronConsole.ConsoleCommandResponse(response); - - } - - - /// - /// Console Command that returns all valid secrets in the essentials program. - /// - /// - /// - /// ListProviders method - /// - public static void ListProviders(string cmd) - { - var response = String.Empty; - var args = cmd.Split(' '); - - if (cmd.Length == 0) - { - if (Secrets != null && Secrets.Count > 0) - { - response = Secrets.Aggregate(response, - (current, secretProvider) => current + (secretProvider.Key + "\n\r")); - } - else - { - response = "No Secrets Providers Available"; - } - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - if (args.Length == 1 && args[0] == "?") - { - response = "Reports all valid and preset Secret providers"; - CrestronConsole.ConsoleCommandResponse(response); - return; - } - - - response = "Improper number of arguments"; - CrestronConsole.ConsoleCommandResponse(response); - - } - - /// - /// Add secret provider to secrets dictionary - /// - /// Key of new entry - /// New Provider Entry - /// - /// AddSecretProvider method - /// - public static void AddSecretProvider(string key, ISecretProvider provider) - { - if (!Secrets.ContainsKey(key)) - { - Secrets.Add(key, provider); - Debug.LogMessage(LogEventLevel.Debug, "Secrets provider '{0}' added to SecretsManager", key); - return; - } - Debug.LogMessage(LogEventLevel.Information, "Unable to add Provider '{0}' to Secrets. Provider with that key already exists", key ); - } - - /// - /// Add secret provider to secrets dictionary, with optional overwrite parameter - /// - /// Key of new entry - /// New provider entry - /// true to overwrite any existing providers in the dictionary - /// - /// AddSecretProvider method - /// - public static void AddSecretProvider(string key, ISecretProvider provider, bool overwrite) - { - if (!Secrets.ContainsKey(key)) - { - Secrets.Add(key, provider); - Debug.LogMessage(LogEventLevel.Debug, "Secrets provider '{0}' added to SecretsManager", key); - return; - } - if (overwrite) - { - Secrets.Add(key, provider); - Debug.LogMessage(LogEventLevel.Debug, "Provider with the key '{0}' already exists in secrets. Overwriting with new secrets provider.", key); - return; - } - Debug.LogMessage(LogEventLevel.Information, "Unable to add Provider '{0}' to Secrets. Provider with that key already exists", key); - } - - private static void SetSecretProcess(string cmd) - { - string response; - var args = cmd.Split(' '); - - if (args.Length == 0) - { - //some Instructional Text - response = "Adds secrets to secret provider. Format 'setsecret '"; - CrestronConsole.ConsoleCommandResponse(response); - return; - } - - if (args.Length == 1 && args[0] == "?") - { - response = "Adds secrets to secret provider. Format 'setsecret '"; - CrestronConsole.ConsoleCommandResponse(response); - return; - } - - if (args.Length < 3) - { - response = "Improper number of arguments"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - var provider = GetSecretProviderByKey(args[0]); - - if (provider == null) - { - //someFail - response = "Provider key invalid"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - var key = args[1]; - var secret = args[2]; - - CrestronConsole.ConsoleCommandResponse(SetSecret(provider, key, secret)); - } - - private static void UpdateSecretProcess(string cmd) - { - string response; - var args = cmd.Split(' '); - - if (args.Length == 0) - { - //some Instructional Text - response = "Updates secrets in secret provider. Format 'updatesecret '"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - if (args.Length == 1 && args[0] == "?") - { - response = "Updates secrets in secret provider. Format 'updatesecret '"; - CrestronConsole.ConsoleCommandResponse(response); - return; - } - - - if (args.Length < 3) - { - //someFail - response = "Improper number of arguments"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - var provider = GetSecretProviderByKey(args[0]); - - if (provider == null) - { - //someFail - response = "Provider key invalid"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - var key = args[1]; - var secret = args[2]; - - CrestronConsole.ConsoleCommandResponse(UpdateSecret(provider, key, secret)); - - } - - private static string UpdateSecret(ISecretProvider provider, string key, string secret) - { - var secretPresent = provider.TestSecret(key); - - Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key); - - if (!secretPresent) - return - String.Format( - "Unable to update secret for {0}:{1} - Please use the 'SetSecret' command to modify it"); - var response = provider.SetSecret(key, secret) - ? String.Format( - "Secret successfully set for {0}:{1}", - provider.Key, key) - : String.Format( - "Unable to set secret for {0}:{1}", - provider.Key, key); - return response; - } - - private static string SetSecret(ISecretProvider provider, string key, string secret) - { - var secretPresent = provider.TestSecret(key); - - Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key); - - if (secretPresent) - return - String.Format( - "Unable to set secret for {0}:{1} - Please use the 'UpdateSecret' command to modify it"); - var response = provider.SetSecret(key, secret) - ? String.Format( - "Secret successfully set for {0}:{1}", - provider.Key, key) - : String.Format( - "Unable to set secret for {0}:{1}", - provider.Key, key); - return response; - - } - - private static void DeleteSecretProcess(string cmd) - { - string response; - var args = cmd.Split(' '); - - if (args.Length == 0) - { - //some Instructional Text - response = "Deletes secrets in secret provider. Format 'deletesecret '"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - if (args.Length == 1 && args[0] == "?") - { - response = "Deletes secrets in secret provider. Format 'deletesecret '"; - CrestronConsole.ConsoleCommandResponse(response); - return; - } - - - - if (args.Length < 2) - { - //someFail - response = "Improper number of arguments"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - var provider = GetSecretProviderByKey(args[0]); - - if (provider == null) - { - //someFail - response = "Provider key invalid"; - CrestronConsole.ConsoleCommandResponse(response); - return; - - } - - var key = args[1]; - - - provider.SetSecret(key, ""); - response = provider.SetSecret(key, "") - ? String.Format( - "Secret successfully deleted for {0}:{1}", - provider.Key, key) - : String.Format( - "Unable to delete secret for {0}:{1}", - provider.Key, key); - CrestronConsole.ConsoleCommandResponse(response); - return; - - + Debug.LogError(e, "SecretsManager Initialize failed"); } } + static SecretsManager() + { + Secrets = new Dictionary(); + } + /// + /// Get Secret Provider from dictionary by key + /// + /// Dictionary Key for provider + /// ISecretProvider + public static ISecretProvider GetSecretProviderByKey(string key) + { + ISecretProvider secret; + + Secrets.TryGetValue(key, out secret); + + if (secret == null) + { + Debug.LogMessage(LogEventLevel.Debug, "SecretsManager unable to retrieve SecretProvider with the key '{0}'", key); + } + return secret; + } + + public static void GetProviderInfo(string cmd) + { + string response; + var args = cmd.Split(' '); + + if (cmd.Length == 0 || (args.Length == 1 && args[0] == "?")) + { + response = "Returns data about secrets provider. Format 'secretproviderinfo '"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + if (args.Length == 1) + { + var provider = GetSecretProviderByKey(args[0]); + + if (provider == null) + { + response = "Invalid secrets provider key"; + CrestronConsole.ConsoleCommandResponse(response); + return; + } + + response = String.Format("{0} : {1}", provider.Key, provider.Description); + CrestronConsole.ConsoleCommandResponse(response); + return; + } + + response = "Improper number of arguments"; + CrestronConsole.ConsoleCommandResponse(response); + + } + + + /// + /// Console Command that returns all valid secrets in the essentials program. + /// + /// + public static void ListProviders(string cmd) + { + var response = String.Empty; + var args = cmd.Split(' '); + + if (cmd.Length == 0) + { + if (Secrets != null && Secrets.Count > 0) + { + response = Secrets.Aggregate(response, + (current, secretProvider) => current + (secretProvider.Key + "\n\r")); + } + else + { + response = "No Secrets Providers Available"; + } + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + if (args.Length == 1 && args[0] == "?") + { + response = "Reports all valid and preset Secret providers"; + CrestronConsole.ConsoleCommandResponse(response); + return; + } + + + response = "Improper number of arguments"; + CrestronConsole.ConsoleCommandResponse(response); + + } + + /// + /// Add secret provider to secrets dictionary + /// + /// Key of new entry + /// New Provider Entry + public static void AddSecretProvider(string key, ISecretProvider provider) + { + if (!Secrets.ContainsKey(key)) + { + Secrets.Add(key, provider); + Debug.LogMessage(LogEventLevel.Debug, "Secrets provider '{0}' added to SecretsManager", key); + return; + } + Debug.LogMessage(LogEventLevel.Information, "Unable to add Provider '{0}' to Secrets. Provider with that key already exists", key ); + } + + /// + /// Add secret provider to secrets dictionary, with optional overwrite parameter + /// + /// Key of new entry + /// New provider entry + /// true to overwrite any existing providers in the dictionary + public static void AddSecretProvider(string key, ISecretProvider provider, bool overwrite) + { + if (!Secrets.ContainsKey(key)) + { + Secrets.Add(key, provider); + Debug.LogMessage(LogEventLevel.Debug, "Secrets provider '{0}' added to SecretsManager", key); + return; + } + if (overwrite) + { + Secrets.Add(key, provider); + Debug.LogMessage(LogEventLevel.Debug, "Provider with the key '{0}' already exists in secrets. Overwriting with new secrets provider.", key); + return; + } + Debug.LogMessage(LogEventLevel.Information, "Unable to add Provider '{0}' to Secrets. Provider with that key already exists", key); + } + + private static void SetSecretProcess(string cmd) + { + string response; + var args = cmd.Split(' '); + + if (args.Length == 0) + { + //some Instructional Text + response = "Adds secrets to secret provider. Format 'setsecret '"; + CrestronConsole.ConsoleCommandResponse(response); + return; + } + + if (args.Length == 1 && args[0] == "?") + { + response = "Adds secrets to secret provider. Format 'setsecret '"; + CrestronConsole.ConsoleCommandResponse(response); + return; + } + + if (args.Length < 3) + { + response = "Improper number of arguments"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + var provider = GetSecretProviderByKey(args[0]); + + if (provider == null) + { + //someFail + response = "Provider key invalid"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + var key = args[1]; + var secret = args[2]; + + CrestronConsole.ConsoleCommandResponse(SetSecret(provider, key, secret)); + } + + private static void UpdateSecretProcess(string cmd) + { + string response; + var args = cmd.Split(' '); + + if (args.Length == 0) + { + //some Instructional Text + response = "Updates secrets in secret provider. Format 'updatesecret '"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + if (args.Length == 1 && args[0] == "?") + { + response = "Updates secrets in secret provider. Format 'updatesecret '"; + CrestronConsole.ConsoleCommandResponse(response); + return; + } + + + if (args.Length < 3) + { + //someFail + response = "Improper number of arguments"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + var provider = GetSecretProviderByKey(args[0]); + + if (provider == null) + { + //someFail + response = "Provider key invalid"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + var key = args[1]; + var secret = args[2]; + + CrestronConsole.ConsoleCommandResponse(UpdateSecret(provider, key, secret)); + + } + + private static string UpdateSecret(ISecretProvider provider, string key, string secret) + { + var secretPresent = provider.TestSecret(key); + + Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key); + + if (!secretPresent) + return + String.Format( + "Unable to update secret for {0}:{1} - Please use the 'SetSecret' command to modify it"); + var response = provider.SetSecret(key, secret) + ? String.Format( + "Secret successfully set for {0}:{1}", + provider.Key, key) + : String.Format( + "Unable to set secret for {0}:{1}", + provider.Key, key); + return response; + } + + private static string SetSecret(ISecretProvider provider, string key, string secret) + { + var secretPresent = provider.TestSecret(key); + + Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key); + + if (secretPresent) + return + String.Format( + "Unable to set secret for {0}:{1} - Please use the 'UpdateSecret' command to modify it"); + var response = provider.SetSecret(key, secret) + ? String.Format( + "Secret successfully set for {0}:{1}", + provider.Key, key) + : String.Format( + "Unable to set secret for {0}:{1}", + provider.Key, key); + return response; + + } + + private static void DeleteSecretProcess(string cmd) + { + string response; + var args = cmd.Split(' '); + + if (args.Length == 0) + { + //some Instructional Text + response = "Deletes secrets in secret provider. Format 'deletesecret '"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + if (args.Length == 1 && args[0] == "?") + { + response = "Deletes secrets in secret provider. Format 'deletesecret '"; + CrestronConsole.ConsoleCommandResponse(response); + return; + } + + + + if (args.Length < 2) + { + //someFail + response = "Improper number of arguments"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + var provider = GetSecretProviderByKey(args[0]); + + if (provider == null) + { + //someFail + response = "Provider key invalid"; + CrestronConsole.ConsoleCommandResponse(response); + return; + + } + + var key = args[1]; + + + provider.SetSecret(key, ""); + response = provider.SetSecret(key, "") + ? String.Format( + "Secret successfully deleted for {0}:{1}", + provider.Key, key) + : String.Format( + "Unable to delete secret for {0}:{1}", + provider.Key, key); + CrestronConsole.ConsoleCommandResponse(response); + return; + + + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Secrets/SecretsPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Secrets/SecretsPropertiesConfig.cs index a5a76074..b447851a 100644 --- a/src/PepperDash.Essentials.Core/Secrets/SecretsPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Secrets/SecretsPropertiesConfig.cs @@ -7,23 +7,15 @@ using System.Text; using Crestron.SimplSharp; using Newtonsoft.Json; -namespace PepperDash.Essentials.Core -{ - /// - /// Provide a way to easily deserialize into a secret object from config - /// - public class SecretsPropertiesConfig - { - /// - /// Gets or sets the Provider - /// - [JsonProperty("provider")] - public string Provider { get; set; } +namespace PepperDash.Essentials.Core; - /// - /// Gets or sets the Key - /// - [JsonProperty("key")] - public string Key { get; set; } - } +/// +/// Provide a way to easily deserialize into a secret object from config +/// +public class SecretsPropertiesConfig +{ + [JsonProperty("provider")] + public string Provider { get; set; } + [JsonProperty("key")] + public string Key { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Shades/Shade Interfaces.cs b/src/PepperDash.Essentials.Core/Shades/Shade Interfaces.cs index 2fb5ffd7..08abeaa8 100644 --- a/src/PepperDash.Essentials.Core/Shades/Shade Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Shades/Shade Interfaces.cs @@ -1,178 +1,177 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Core.Shades +namespace PepperDash.Essentials.Core.Shades; + +/// +/// Defines the contract for IShades +/// +public interface IShades { - /// - /// Defines the contract for IShades - /// - public interface IShades - { - /// - /// List of shades controlled by this device - /// - List Shades { get; } - } + /// + /// List of shades controlled by this device + /// + List Shades { get; } +} - /// - /// Requirements for a device that implements basic Open/Close/Stop shade control (Uses 3 relays) - /// - public interface IShadesOpenCloseStop - { - /// - /// Opens the shade - /// - void Open(); - - /// - /// Closes the shade - /// - void Close(); - - /// - /// Stops the shade - /// - void Stop(); - } +/// +/// Requirements for a device that implements basic Open/Close/Stop shade control (Uses 3 relays) +/// +public interface IShadesOpenCloseStop +{ + /// + /// Opens the shade + /// + void Open(); /// - /// Requirements for a device that implements Open/Close/Stop shade control with presets + /// Closes the shade /// - public interface IShadesOpenClosePreset : IShadesOpenCloseStop - { - /// - /// Recalls the preset - /// - /// preset number to recall - void RecallPreset(uint presetNumber); - - /// - /// Saves the preset - /// - /// preset number to save - void SavePreset(uint presetNumber); - - /// - /// Label for the preset button - /// - string StopOrPresetButtonLabel { get; } - - /// - /// Event raised when a preset is recalled - /// - event EventHandler PresetSaved; - } - - - /// - /// Defines the contract for IShadesRaiseLowerFeedback - /// - public interface IShadesRaiseLowerFeedback - { - /// - /// Feedback to indicate if the shade is lowering - /// - BoolFeedback ShadeIsLoweringFeedback { get; } - - /// - /// Feedback to indicate if the shade is raising - /// - BoolFeedback ShadeIsRaisingFeedback { get; } - } + void Close(); /// - /// Requirements for a shade/scene that is open or closed + /// Stops the shade /// - public interface IShadesOpenClosedFeedback: IShadesOpenCloseStop - { - /// - /// Feedback to indicate if the shade is open - /// - BoolFeedback ShadeIsOpenFeedback { get; } + void Stop(); +} - /// - /// Feedback to indicate if the shade is closed - /// - BoolFeedback ShadeIsClosedFeedback { get; } - } +/// +/// Requirements for a device that implements Open/Close/Stop shade control with presets +/// +public interface IShadesOpenClosePreset : IShadesOpenCloseStop +{ + /// + /// Recalls the preset + /// + /// preset number to recall + void RecallPreset(uint presetNumber); /// - /// Used to implement raise/stop/lower/stop from single button + /// Saves the preset /// - public interface IShadesStopOrMove - { - /// - /// Raises the shade or stops it if it's already moving - /// - void OpenOrStop(); - - /// - /// Lowers the shade or stops it if it's already moving - /// - void CloseOrStop(); - - /// - /// Opens, closes, or stops the shade depending on current state - /// - void OpenCloseOrStop(); - } - - /// - /// Defines the contract for IShadesStopFeedback - /// - public interface IShadesStopFeedback : IShadesOpenCloseStop - { - /// - /// Feedback to indicate if the shade is stopped - /// - BoolFeedback IsStoppedFeedback { get; } - } - - /// - /// Requirements for position - /// - public interface IShadesPosition - { - /// - /// Gets the current position of the shade - /// - /// value of the position to set - void SetPosition(ushort value); - } + /// preset number to save + void SavePreset(uint presetNumber); /// - /// Basic feedback for shades position + /// Label for the preset button /// - public interface IShadesFeedback: IShadesPosition, IShadesStopFeedback - { - /// - /// Feedback to indicate the current position of the shade - /// - IntFeedback PositionFeedback { get; } - } + string StopOrPresetButtonLabel { get; } /// - /// Feedback for scenes + /// Event raised when a preset is recalled /// - public interface ISceneFeedback - { - /// - /// Runs the scene - /// - void Run(); + event EventHandler PresetSaved; +} - /// - /// Feedback to indicate if all shades are at the scene position - /// - BoolFeedback AllAreAtSceneFeedback { get; } - } + +/// +/// Defines the contract for IShadesRaiseLowerFeedback +/// +public interface IShadesRaiseLowerFeedback +{ + /// + /// Feedback to indicate if the shade is lowering + /// + BoolFeedback ShadeIsLoweringFeedback { get; } /// - /// Combines basic shade interfaces for Crestron Basic shades + /// Feedback to indicate if the shade is raising /// - public interface ICrestronBasicShade : IShadesOpenClosedFeedback, - IShadesStopOrMove, IShadesFeedback, IShadesRaiseLowerFeedback - { + BoolFeedback ShadeIsRaisingFeedback { get; } +} - } -} \ No newline at end of file +/// +/// Requirements for a shade/scene that is open or closed +/// +public interface IShadesOpenClosedFeedback : IShadesOpenCloseStop +{ + /// + /// Feedback to indicate if the shade is open + /// + BoolFeedback ShadeIsOpenFeedback { get; } + + /// + /// Feedback to indicate if the shade is closed + /// + BoolFeedback ShadeIsClosedFeedback { get; } +} + +/// +/// Used to implement raise/stop/lower/stop from single button +/// +public interface IShadesStopOrMove +{ + /// + /// Raises the shade or stops it if it's already moving + /// + void OpenOrStop(); + + /// + /// Lowers the shade or stops it if it's already moving + /// + void CloseOrStop(); + + /// + /// Opens, closes, or stops the shade depending on current state + /// + void OpenCloseOrStop(); +} + +/// +/// Defines the contract for IShadesStopFeedback +/// +public interface IShadesStopFeedback : IShadesOpenCloseStop +{ + /// + /// Feedback to indicate if the shade is stopped + /// + BoolFeedback IsStoppedFeedback { get; } +} + +/// +/// Requirements for position +/// +public interface IShadesPosition +{ + /// + /// Gets the current position of the shade + /// + /// value of the position to set + void SetPosition(ushort value); +} + +/// +/// Basic feedback for shades position +/// +public interface IShadesFeedback : IShadesPosition, IShadesStopFeedback +{ + /// + /// Feedback to indicate the current position of the shade + /// + IntFeedback PositionFeedback { get; } +} + +/// +/// Feedback for scenes +/// +public interface ISceneFeedback +{ + /// + /// Runs the scene + /// + void Run(); + + /// + /// Feedback to indicate if all shades are at the scene position + /// + BoolFeedback AllAreAtSceneFeedback { get; } +} + +/// +/// Combines basic shade interfaces for Crestron Basic shades +/// +public interface ICrestronBasicShade : IShadesOpenClosedFeedback, + IShadesStopOrMove, IShadesFeedback, IShadesRaiseLowerFeedback +{ + +} diff --git a/src/PepperDash.Essentials.Core/Shades/ShadeBase.cs b/src/PepperDash.Essentials.Core/Shades/ShadeBase.cs index ce5de98d..c0808989 100644 --- a/src/PepperDash.Essentials.Core/Shades/ShadeBase.cs +++ b/src/PepperDash.Essentials.Core/Shades/ShadeBase.cs @@ -7,42 +7,42 @@ using Crestron.SimplSharp; using PepperDash.Core; using PepperDash.Essentials.Core.CrestronIO; -namespace PepperDash.Essentials.Core.Shades +namespace PepperDash.Essentials.Core.Shades; + +/// +/// Base class for shades +/// +[Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")] +public abstract class ShadeBase : EssentialsDevice, IShadesOpenCloseStop { /// - /// Base class for shades + /// Constructor /// - [Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")] - public abstract class ShadeBase : EssentialsDevice, IShadesOpenCloseStop + /// key of the shade device + /// name of the shade device + public ShadeBase(string key, string name) + : base(key, name) { - /// - /// Constructor - /// - /// key of the shade device - /// name of the shade device - public ShadeBase(string key, string name) - : base(key, name) - { - } - - #region iShadesOpenClose Members - - /// - /// Opens the shade - /// - public abstract void Open(); - - /// - /// Stops the shade - /// - public abstract void Stop(); - - /// - /// Closes the shade - /// - public abstract void Close(); - - #endregion } -} \ No newline at end of file + + #region iShadesOpenClose Members + + /// + /// Opens the shade + /// + public abstract void Open(); + + /// + /// Stops the shade + /// + public abstract void Stop(); + + /// + /// Closes the shade + /// + public abstract void Close(); + + #endregion +} + diff --git a/src/PepperDash.Essentials.Core/SigHelper.cs b/src/PepperDash.Essentials.Core/SigHelper.cs index df1ad605..3aa1bbc8 100644 --- a/src/PepperDash.Essentials.Core/SigHelper.cs +++ b/src/PepperDash.Essentials.Core/SigHelper.cs @@ -7,8 +7,8 @@ using Crestron.SimplSharpPro; using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Helper class for various Sig events /// @@ -75,5 +75,4 @@ namespace PepperDash.Essentials.Core { sig.CreateRamp(level, time / 10); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDPad.cs b/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDPad.cs index 605b2344..011b3f97 100644 --- a/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDPad.cs +++ b/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDPad.cs @@ -6,46 +6,45 @@ using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core.SmartObjects +namespace PepperDash.Essentials.Core.SmartObjects; + +/// +/// Represents a SmartObjectDPad +/// +public class SmartObjectDPad : SmartObjectHelperBase { - /// - /// Represents a SmartObjectDPad - /// - public class SmartObjectDPad : SmartObjectHelperBase - { - /// - /// Gets or sets the SigUp - /// - public BoolOutputSig SigUp { get { return GetBoolOutputNamed("Up"); } } + /// + /// Gets or sets the SigUp + /// + public BoolOutputSig SigUp { get { return GetBoolOutputNamed("Up"); } } - /// - /// Gets or sets the SigDown - /// - public BoolOutputSig SigDown { get { return GetBoolOutputNamed("Down"); } } + /// + /// Gets or sets the SigDown + /// + public BoolOutputSig SigDown { get { return GetBoolOutputNamed("Down"); } } - /// - /// Gets or sets the SigLeft - /// - public BoolOutputSig SigLeft { get { return GetBoolOutputNamed("Left"); } } + /// + /// Gets or sets the SigLeft + /// + public BoolOutputSig SigLeft { get { return GetBoolOutputNamed("Left"); } } - /// - /// Gets or sets the SigRight - /// - public BoolOutputSig SigRight { get { return GetBoolOutputNamed("Right"); } } + /// + /// Gets or sets the SigRight + /// + public BoolOutputSig SigRight { get { return GetBoolOutputNamed("Right"); } } - /// - /// Gets or sets the SigCenter - /// - public BoolOutputSig SigCenter { get { return GetBoolOutputNamed("Center"); } } + /// + /// Gets or sets the SigCenter + /// + public BoolOutputSig SigCenter { get { return GetBoolOutputNamed("Center"); } } - /// - /// Constructor - /// - /// smart object - /// use user object handler if true - public SmartObjectDPad(SmartObject so, bool useUserObjectHandler) - : base(so, useUserObjectHandler) - { - } - } + /// + /// Constructor + /// + /// smart object + /// use user object handler if true + public SmartObjectDPad(SmartObject so, bool useUserObjectHandler) + : base(so, useUserObjectHandler) + { + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDynamicList.cs b/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDynamicList.cs index fdf7dc04..d83d3aa5 100644 --- a/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDynamicList.cs +++ b/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDynamicList.cs @@ -1,153 +1,148 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; - using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.SmartObjects +namespace PepperDash.Essentials.Core.SmartObjects; + +/// +/// Represents a SmartObjectDynamicList +/// +public class SmartObjectDynamicList : SmartObjectHelperBase { - /// - /// Represents a SmartObjectDynamicList - /// - public class SmartObjectDynamicList : SmartObjectHelperBase + /// + /// Sig name for Scroll To Item + /// + public const string SigNameScrollToItem = "Scroll To Item"; + + /// + /// Sig name for Set Number of Items + /// + public const string SigNameSetNumberOfItems = "Set Number of Items"; + + /// + /// Gets or sets the NameSigOffset + /// + public uint NameSigOffset { get; private set; } + + /// + /// Gets or sets the Count + /// + public ushort Count { - /// - /// Sig name for Scroll To Item - /// - public const string SigNameScrollToItem = "Scroll To Item"; - - /// - /// Sig name for Set Number of Items - /// - public const string SigNameSetNumberOfItems = "Set Number of Items"; - - /// - /// Gets or sets the NameSigOffset - /// - public uint NameSigOffset { get; private set; } - - /// - /// Gets or sets the Count - /// - public ushort Count + get { - get - { - return SmartObject.UShortInput[SigNameSetNumberOfItems].UShortValue; - } - set { SmartObject.UShortInput[SigNameSetNumberOfItems].UShortValue = value; } + return SmartObject.UShortInput[SigNameSetNumberOfItems].UShortValue; } + set { SmartObject.UShortInput[SigNameSetNumberOfItems].UShortValue = value; } + } - /// - /// Gets or sets the MaxCount - /// - public int MaxCount { get; private set; } + /// + /// Gets or sets the MaxCount + /// + public int MaxCount { get; private set; } - /// - /// Wrapper for smart object - /// - /// - /// True if the standard user object action handler will be used - /// The starting join of the string sigs for the button labels - public SmartObjectDynamicList(SmartObject so, bool useUserObjectHandler, uint nameSigOffset) : base(so, useUserObjectHandler) - { - try - { - // Just try to touch the count signal to make sure this is indeed a dynamic list - var c = Count; - NameSigOffset = nameSigOffset; - MaxCount = SmartObject.BooleanOutput.Count(s => s.Name.EndsWith("Pressed")); - //Debug.LogMessage(LogEventLevel.Verbose, "Smart object {0} has {1} max", so.ID, MaxCount); - } - catch - { - var msg = string.Format("SmartObjectDynamicList: Smart Object {0:X2}-{1} is not a dynamic list. Ignoring", so.Device.ID, so.ID); - Debug.LogMessage(LogEventLevel.Information, msg); - } - } - - /// - /// SetItem method - /// - public void SetItem(uint index, string mainText, string iconName, Action action) + /// + /// Wrapper for smart object + /// + /// + /// True if the standard user object action handler will be used + /// The starting join of the string sigs for the button labels + public SmartObjectDynamicList(SmartObject so, bool useUserObjectHandler, uint nameSigOffset) : base(so, useUserObjectHandler) + { + try { - SetItemMainText(index, mainText); - SetItemIcon(index, iconName); - SetItemButtonAction(index, action); - //try - //{ - // SetMainButtonText(index, text); - // SetIcon(index, iconName); - // SetButtonAction(index, action); - //} - //catch(Exception e) - //{ - // Debug.LogMessage(LogEventLevel.Debug, "Cannot set Dynamic List item {0} on smart object {1}", index, SmartObject.ID); - // ErrorLog.Warn(e.ToString()); - //} + // Just try to touch the count signal to make sure this is indeed a dynamic list + var c = Count; + NameSigOffset = nameSigOffset; + MaxCount = SmartObject.BooleanOutput.Count(s => s.Name.EndsWith("Pressed")); + //Debug.LogMessage(LogEventLevel.Verbose, "Smart object {0} has {1} max", so.ID, MaxCount); } - - /// - /// SetItemMainText method - /// - public void SetItemMainText(uint index, string text) + catch { - if (index > MaxCount) return; - // The list item template defines CIPS tags that refer to standard joins - (SmartObject.Device as BasicTriList).StringInput[NameSigOffset + index].StringValue = text; - } - - /// - /// SetItemIcon method - /// - public void SetItemIcon(uint index, string iconName) - { - if (index > MaxCount) return; - SmartObject.StringInput[string.Format("Set Item {0} Icon Serial", index)].StringValue = iconName; - } - - /// - /// SetItemButtonAction method - /// - public void SetItemButtonAction(uint index, Action action) - { - if (index > MaxCount) return; - SmartObject.BooleanOutput[string.Format("Item {0} Pressed", index)].UserObject = action; - } - - /// - /// SetFeedback method - /// - public void SetFeedback(uint index, bool interlocked) - { - if (interlocked) - ClearFeedbacks(); - SmartObject.BooleanInput[string.Format("Item {0} Selected", index)].BoolValue = true; - } - - /// - /// ClearFeedbacks method - /// - public void ClearFeedbacks() - { - for(int i = 1; i<= Count; i++) - SmartObject.BooleanInput[string.Format("Item {0} Selected", i)].BoolValue = false; - } - - /// - /// Removes Action object from all buttons - /// - public void ClearActions() - { - Debug.LogMessage(LogEventLevel.Verbose, "SO CLEAR"); - for(ushort i = 1; i <= MaxCount; i++) - SmartObject.BooleanOutput[string.Format("Item {0} Pressed", i)].UserObject = null; + var msg = string.Format("SmartObjectDynamicList: Smart Object {0:X2}-{1} is not a dynamic list. Ignoring", so.Device.ID, so.ID); + Debug.LogMessage(LogEventLevel.Information, msg); } } + + /// + /// SetItem method + /// + public void SetItem(uint index, string mainText, string iconName, Action action) + { + SetItemMainText(index, mainText); + SetItemIcon(index, iconName); + SetItemButtonAction(index, action); + //try + //{ + // SetMainButtonText(index, text); + // SetIcon(index, iconName); + // SetButtonAction(index, action); + //} + //catch(Exception e) + //{ + // Debug.LogMessage(LogEventLevel.Debug, "Cannot set Dynamic List item {0} on smart object {1}", index, SmartObject.ID); + // ErrorLog.Warn(e.ToString()); + //} + } + + /// + /// SetItemMainText method + /// + public void SetItemMainText(uint index, string text) + { + if (index > MaxCount) return; + // The list item template defines CIPS tags that refer to standard joins + (SmartObject.Device as BasicTriList).StringInput[NameSigOffset + index].StringValue = text; + } + + /// + /// SetItemIcon method + /// + public void SetItemIcon(uint index, string iconName) + { + if (index > MaxCount) return; + SmartObject.StringInput[string.Format("Set Item {0} Icon Serial", index)].StringValue = iconName; + } + + /// + /// SetItemButtonAction method + /// + public void SetItemButtonAction(uint index, Action action) + { + if (index > MaxCount) return; + SmartObject.BooleanOutput[string.Format("Item {0} Pressed", index)].UserObject = action; + } + + /// + /// SetFeedback method + /// + public void SetFeedback(uint index, bool interlocked) + { + if (interlocked) + ClearFeedbacks(); + SmartObject.BooleanInput[string.Format("Item {0} Selected", index)].BoolValue = true; + } + + /// + /// ClearFeedbacks method + /// + public void ClearFeedbacks() + { + for (int i = 1; i <= Count; i++) + SmartObject.BooleanInput[string.Format("Item {0} Selected", i)].BoolValue = false; + } + + /// + /// Removes Action object from all buttons + /// + public void ClearActions() + { + Debug.LogMessage(LogEventLevel.Verbose, "SO CLEAR"); + for (ushort i = 1; i <= MaxCount; i++) + SmartObject.BooleanOutput[string.Format("Item {0} Pressed", i)].UserObject = null; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectHelperBase.cs b/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectHelperBase.cs index a86687aa..aca01f2c 100644 --- a/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectHelperBase.cs +++ b/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectHelperBase.cs @@ -9,96 +9,91 @@ using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core.SmartObjects +namespace PepperDash.Essentials.Core.SmartObjects; +/// +/// Represents a SmartObjectHelperBase +/// +public class SmartObjectHelperBase { /// - /// Represents a SmartObjectHelperBase + /// Gets or sets the SmartObject /// - public class SmartObjectHelperBase + public SmartObject SmartObject { get; private set; } + + /// + /// Gets or sets the Validated + /// + public bool Validated { get; protected set; } + + /// + /// Constructor + /// + /// smart object + /// use the user object hadnler if true + public SmartObjectHelperBase(SmartObject so, bool useUserObjectHandler) { - /// - /// Gets or sets the SmartObject - /// - public SmartObject SmartObject { get; private set; } - - /// - /// Gets or sets the Validated - /// - public bool Validated { get; protected set; } - - /// - /// Constructor - /// - /// smart object - /// use the user object hadnler if true - public SmartObjectHelperBase(SmartObject so, bool useUserObjectHandler) - { - SmartObject = so; - if (useUserObjectHandler) - { - // Prevent this from double-registering - SmartObject.SigChange -= this.SmartObject_SigChange; - SmartObject.SigChange += this.SmartObject_SigChange; - } - } - - /// - /// Destructor - /// - ~SmartObjectHelperBase() + SmartObject = so; + if (useUserObjectHandler) { + // Prevent this from double-registering SmartObject.SigChange -= this.SmartObject_SigChange; + SmartObject.SigChange += this.SmartObject_SigChange; } - - /// - /// Helper to get a sig name with debugging when fail - /// - /// - /// - public BoolOutputSig GetBoolOutputNamed(string name) - { - if (SmartObject.BooleanOutput.Contains(name)) - return SmartObject.BooleanOutput[name]; - else - Debug.LogMessage(LogEventLevel.Information, "WARNING: Cannot get signal. Smart object {0} on trilist {1:x2} does not contain signal '{2}'", - SmartObject.ID, SmartObject.Device.ID, name); - return null; - } - - /// - /// Sets action on signal after checking for existence. - /// - /// - /// - /// - /// SetBoolAction method - /// - public void SetBoolAction(string name, Action a) - { - if (SmartObject.BooleanOutput.Contains(name)) - SmartObject.BooleanOutput[name].UserObject = a; - else - { - Debug.LogMessage(LogEventLevel.Information, "WARNING: Cannot set action. Smart object {0} on trilist {1:x2} does not contain signal '{2}'", - SmartObject.ID, SmartObject.Device.ID, name); - } - } - - /// - /// Standard Action listener - /// - /// - /// - void SmartObject_SigChange(GenericBase currentDevice, SmartObjectEventArgs args) - { - 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); - } - } + + /// + /// Destructor + /// + ~SmartObjectHelperBase() + { + SmartObject.SigChange -= this.SmartObject_SigChange; + } + + /// + /// Helper to get a sig name with debugging when fail + /// + /// + /// + public BoolOutputSig GetBoolOutputNamed(string name) + { + if (SmartObject.BooleanOutput.Contains(name)) + return SmartObject.BooleanOutput[name]; + else + Debug.LogMessage(LogEventLevel.Information, "WARNING: Cannot get signal. Smart object {0} on trilist {1:x2} does not contain signal '{2}'", + SmartObject.ID, SmartObject.Device.ID, name); + return null; + } + + /// + /// Sets action on signal after checking for existence. + /// + /// + /// + public void SetBoolAction(string name, Action a) + { + if (SmartObject.BooleanOutput.Contains(name)) + SmartObject.BooleanOutput[name].UserObject = a; + else + { + Debug.LogMessage(LogEventLevel.Information, "WARNING: Cannot set action. Smart object {0} on trilist {1:x2} does not contain signal '{2}'", + SmartObject.ID, SmartObject.Device.ID, name); + } + } + + /// + /// Standard Action listener + /// + /// + /// + void SmartObject_SigChange(GenericBase currentDevice, SmartObjectEventArgs args) + { + 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); + } + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectNumeric.cs b/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectNumeric.cs index 2b6e06b8..8dbd1998 100644 --- a/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectNumeric.cs +++ b/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectNumeric.cs @@ -6,92 +6,94 @@ using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core.SmartObjects +namespace PepperDash.Essentials.Core.SmartObjects; + +/// +/// Represents a SmartObjectNumeric +/// This is a helper for a numeric keypad. It has the digits 0-9 and two miscellaneous buttons. The Misc1SigName and Misc2SigName can be set to whatever the user wants, but default to "Misc_1" and "Misc_2". +/// +public class SmartObjectNumeric : SmartObjectHelperBase { /// - /// Represents a SmartObjectNumeric + /// Defaults to "Misc_1". The name of the button in VTPro (Usually the text) /// - public class SmartObjectNumeric : SmartObjectHelperBase + public string Misc1SigName { get; set; } + + + /// + /// Gets or sets the Misc2SigName + /// + public string Misc2SigName { get; set; } + + /// + /// Gets or sets the Digit1 + /// + public BoolOutputSig Digit1 { get { return GetBoolOutputNamed("1"); } } + + /// + /// Gets or sets the Digit2 + /// + public BoolOutputSig Digit2 { get { return GetBoolOutputNamed("2"); } } + + /// + /// Gets or sets the Digit3 + /// + public BoolOutputSig Digit3 { get { return GetBoolOutputNamed("3"); } } + + /// + /// Gets or sets the Digit4 + /// + public BoolOutputSig Digit4 { get { return GetBoolOutputNamed("4"); } } + + /// + /// Gets or sets the Digit5 + /// + public BoolOutputSig Digit5 { get { return GetBoolOutputNamed("5"); } } + + /// + /// Gets or sets the Digit6 + /// + public BoolOutputSig Digit6 { get { return GetBoolOutputNamed("6"); } } + + /// + /// Gets or sets the Digit7 + /// + public BoolOutputSig Digit7 { get { return GetBoolOutputNamed("7"); } } + + /// + /// Gets or sets the Digit8 + /// + public BoolOutputSig Digit8 { get { return GetBoolOutputNamed("8"); } } + + /// + /// Gets or sets the Digit9 + /// + public BoolOutputSig Digit9 { get { return GetBoolOutputNamed("9"); } } + + /// + /// Gets or sets the Digit0 + /// + public BoolOutputSig Digit0 { get { return GetBoolOutputNamed("0"); } } + + /// + /// Gets or sets the Misc1 + /// + public BoolOutputSig Misc1 { get { return GetBoolOutputNamed(Misc1SigName); } } + + /// + /// Gets or sets the Misc2 + /// + public BoolOutputSig Misc2 { get { return GetBoolOutputNamed(Misc2SigName); } } + + /// + /// Constructor + /// + /// smart object + /// use user handler if true + public SmartObjectNumeric(SmartObject so, bool useUserObjectHandler) : base(so, useUserObjectHandler) { - /// - /// Gets or sets the Misc1SigName - /// - public string Misc1SigName { get; set; } - - /// - /// Gets or sets the Misc2SigName - /// - public string Misc2SigName { get; set; } - - /// - /// Gets or sets the Digit1 - /// - public BoolOutputSig Digit1 { get { return GetBoolOutputNamed("1"); } } - - /// - /// Gets or sets the Digit2 - /// - public BoolOutputSig Digit2 { get { return GetBoolOutputNamed("2"); } } - - /// - /// Gets or sets the Digit3 - /// - public BoolOutputSig Digit3 { get { return GetBoolOutputNamed("3"); } } - - /// - /// Gets or sets the Digit4 - /// - public BoolOutputSig Digit4 { get { return GetBoolOutputNamed("4"); } } - - /// - /// Gets or sets the Digit5 - /// - public BoolOutputSig Digit5 { get { return GetBoolOutputNamed("5"); } } - - /// - /// Gets or sets the Digit6 - /// - public BoolOutputSig Digit6 { get { return GetBoolOutputNamed("6"); } } - - /// - /// Gets or sets the Digit7 - /// - public BoolOutputSig Digit7 { get { return GetBoolOutputNamed("7"); } } - - /// - /// Gets or sets the Digit8 - /// - public BoolOutputSig Digit8 { get { return GetBoolOutputNamed("8"); } } - - /// - /// Gets or sets the Digit9 - /// - public BoolOutputSig Digit9 { get { return GetBoolOutputNamed("9"); } } - - /// - /// Gets or sets the Digit0 - /// - public BoolOutputSig Digit0 { get { return GetBoolOutputNamed("0"); } } - - /// - /// Gets or sets the Misc1 - /// - public BoolOutputSig Misc1 { get { return GetBoolOutputNamed(Misc1SigName); } } - - /// - /// Gets or sets the Misc2 - /// - public BoolOutputSig Misc2 { get { return GetBoolOutputNamed(Misc2SigName); } } - - /// - /// Constructor - /// - /// smart object - /// use user handler if true - public SmartObjectNumeric(SmartObject so, bool useUserObjectHandler) : base(so, useUserObjectHandler) - { - Misc1SigName = "Misc_1"; - Misc2SigName = "Misc_2"; - } + Misc1SigName = "Misc_1"; + Misc2SigName = "Misc_2"; } -} \ No newline at end of file +} + diff --git a/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferencList/SubpageReferenceList.cs b/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferencList/SubpageReferenceList.cs index 4ed42d49..d7a5c839 100644 --- a/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferencList/SubpageReferenceList.cs +++ b/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferencList/SubpageReferenceList.cs @@ -1,305 +1,290 @@ - -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; -using Crestron.SimplSharpPro.UI; - using PepperDash.Core; using Serilog.Events; +namespace PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core + +//***************************************************************************** +/// +/// Wrapper class for subpage reference list. Contains helpful methods to get at the various signal groupings +/// and to get individual signals using an index and a join. +/// +public class SubpageReferenceList { - ////***************************************************************************** - ///// - ///// Base class for all subpage reference list controllers - ///// - //public class SubpageReferenceListController - //{ - // public SubpageReferenceList TheList { get; protected set; } - //} - - //***************************************************************************** /// - /// Wrapper class for subpage reference list. Contains helpful methods to get at the various signal groupings - /// and to get individual signals using an index and a join. + /// Gets or sets the Count /// - public class SubpageReferenceList + public ushort Count { - /// - /// Gets or sets the Count - /// - public ushort Count + get { return SetNumberOfItemsSig.UShortValue; } + set { SetNumberOfItemsSig.UShortValue = value; } + } + + /// + /// Gets or sets the MaxDefinedItems + /// + public ushort MaxDefinedItems { get; private set; } + + /// + /// Gets or sets the ScrollToItemSig + /// + public UShortInputSig ScrollToItemSig { get; private set; } + + UShortInputSig SetNumberOfItemsSig; + + /// + /// Gets or sets the BoolIncrement + /// + public uint BoolIncrement { get; protected set; } + + /// + /// Gets or sets the UShortIncrement + /// + public uint UShortIncrement { get; protected set; } + + /// + /// Gets or sets the StringIncrement + /// + public uint StringIncrement { get; protected set; } + + /// + /// Gets or sets the SRL + /// + protected readonly SmartObject SRL; + + /// + /// Gets the list of items in the SRL + /// + protected readonly List Items = new List(); + + /// + /// Constructor + /// + /// trilist for the smart object + /// smart object ID + /// + /// + /// + public SubpageReferenceList(BasicTriListWithSmartObject triList, uint smartObjectId, + uint boolIncrement, uint ushortIncrement, uint stringIncrement) + { + SmartObject obj; + // Fail cleanly if not defined + if (triList.SmartObjects == null || triList.SmartObjects.Count == 0) { - get { return SetNumberOfItemsSig.UShortValue; } - set { SetNumberOfItemsSig.UShortValue = value; } + Debug.LogMessage(LogEventLevel.Information, "TriList {0:X2} Smart objects have not been loaded", triList.ID, smartObjectId); + return; } - - /// - /// Gets or sets the MaxDefinedItems - /// - public ushort MaxDefinedItems { get; private set; } - - /// - /// Gets or sets the ScrollToItemSig - /// - public UShortInputSig ScrollToItemSig { get; private set; } - - UShortInputSig SetNumberOfItemsSig; - - /// - /// Gets or sets the BoolIncrement - /// - public uint BoolIncrement { get; protected set; } - - /// - /// Gets or sets the UShortIncrement - /// - public uint UShortIncrement { get; protected set; } - - /// - /// Gets or sets the StringIncrement - /// - public uint StringIncrement { get; protected set; } - - /// - /// Gets or sets the SRL - /// - protected readonly SmartObject SRL; - - /// - /// Gets the list of items in the SRL - /// - protected readonly List Items = new List(); - - /// - /// Constructor - /// - /// trilist for the smart object - /// smart object ID - /// - /// - /// - public SubpageReferenceList(BasicTriListWithSmartObject triList, uint smartObjectId, - uint boolIncrement, uint ushortIncrement, uint stringIncrement) + if (triList.SmartObjects.TryGetValue(smartObjectId, out obj)) { - SmartObject obj; - // Fail cleanly if not defined - if (triList.SmartObjects == null || triList.SmartObjects.Count == 0) - { - Debug.LogMessage(LogEventLevel.Information, "TriList {0:X2} Smart objects have not been loaded", triList.ID, smartObjectId); - return; - } - if (triList.SmartObjects.TryGetValue(smartObjectId, out obj)) - { - SRL = triList.SmartObjects[smartObjectId]; - ScrollToItemSig = SRL.UShortInput["Scroll To Item"]; - SetNumberOfItemsSig = SRL.UShortInput["Set Number of Items"]; - BoolIncrement = boolIncrement; - UShortIncrement = ushortIncrement; - StringIncrement = stringIncrement; + SRL = triList.SmartObjects[smartObjectId]; + ScrollToItemSig = SRL.UShortInput["Scroll To Item"]; + SetNumberOfItemsSig = SRL.UShortInput["Set Number of Items"]; + BoolIncrement = boolIncrement; + UShortIncrement = ushortIncrement; + StringIncrement = stringIncrement; - // Count the enable lines to see what max items is - MaxDefinedItems = (ushort)SRL.BooleanInput - .Where(s => s.Name.Contains("Enable")).Count(); - Debug.LogMessage(LogEventLevel.Verbose, "SRL {0} contains max {1} items", SRL.ID, MaxDefinedItems); + // Count the enable lines to see what max items is + MaxDefinedItems = (ushort)SRL.BooleanInput + .Where(s => s.Name.Contains("Enable")).Count(); + Debug.LogMessage(LogEventLevel.Verbose, "SRL {0} contains max {1} items", SRL.ID, MaxDefinedItems); - SRL.SigChange -= new SmartObjectSigChangeEventHandler(SRL_SigChange); - SRL.SigChange += new SmartObjectSigChangeEventHandler(SRL_SigChange); - } - else - Debug.LogMessage(LogEventLevel.Information, "ERROR: TriList 0x{0:X2} Cannot load smart object {1}. Verify correct SGD file is loaded", - triList.ID, smartObjectId); + SRL.SigChange -= new SmartObjectSigChangeEventHandler(SRL_SigChange); + SRL.SigChange += new SmartObjectSigChangeEventHandler(SRL_SigChange); } + else + Debug.LogMessage(LogEventLevel.Information, "ERROR: TriList 0x{0:X2} Cannot load smart object {1}. Verify correct SGD file is loaded", + triList.ID, smartObjectId); + } - /// - /// Adds item to saved list of displayed items (not necessarily in order) - /// DOES NOT adjust Count - /// - /// - /// - /// AddItem method - /// - public void AddItem(SubpageReferenceListItem item) - { - Items.Add(item); - } + /// + /// Adds item to saved list of displayed items (not necessarily in order) + /// DOES NOT adjust Count + /// + /// + /// + /// AddItem method + /// + public void AddItem(SubpageReferenceListItem item) + { + Items.Add(item); + } - /// - /// Clear method - /// - public void Clear() - { - // If a line item needs to disconnect an CueActionPair or do something to release RAM - foreach (var item in Items) item.Clear(); - // Empty the list - Items.Clear(); - // Clean up the SRL - Count = 1; + /// + /// Clear method + /// + public void Clear() + { + // If a line item needs to disconnect an CueActionPair or do something to release RAM + foreach (var item in Items) item.Clear(); + // Empty the list + Items.Clear(); + // Clean up the SRL + Count = 1; - ScrollToItemSig.UShortValue = 1; - } + ScrollToItemSig.UShortValue = 1; + } - /// - /// Refresh method - /// - public void Refresh() - { - foreach (var item in Items) item.Refresh(); - } + /// + /// Refresh method + /// + public void Refresh() + { + foreach (var item in Items) item.Refresh(); + } - // Helpers to get sigs by their weird SO names + // Helpers to get sigs by their weird SO names - /// - /// Returns the Sig associated with a given SRL line index - /// and the join number of the object on the SRL subpage. - /// Note: If the join number exceeds the increment range, or the count of Sigs on the - /// list object, this will return null - /// - /// The line or item position on the SRL - /// The join number of the item on the SRL subpage - /// A Sig or null if the numbers are out of range - public BoolOutputSig GetBoolFeedbackSig(uint index, uint sigNum) - { - if (sigNum > BoolIncrement) return null; - return SRL.BooleanOutput.FirstOrDefault(s => s.Name.Equals(GetBoolFeedbackSigName(index, sigNum))); - } + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line or item position on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public BoolOutputSig GetBoolFeedbackSig(uint index, uint sigNum) + { + if (sigNum > BoolIncrement) return null; + return SRL.BooleanOutput.FirstOrDefault(s => s.Name.Equals(GetBoolFeedbackSigName(index, sigNum))); + } - /// - /// Returns the Sig associated with a given SRL line index - /// and the join number of the object on the SRL subpage. - /// Note: If the join number exceeds the increment range, or the count of Sigs on the - /// list object, this will return null - /// - /// The line or item position on the SRL - /// The join number of the item on the SRL subpage - /// A Sig or null if the numbers are out of range - public UShortOutputSig GetUShortOutputSig(uint index, uint sigNum) - { - if (sigNum > UShortIncrement) return null; - return SRL.UShortOutput.FirstOrDefault(s => s.Name.Equals(GetUShortOutputSigName(index, sigNum))); - } + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line or item position on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public UShortOutputSig GetUShortOutputSig(uint index, uint sigNum) + { + if (sigNum > UShortIncrement) return null; + return SRL.UShortOutput.FirstOrDefault(s => s.Name.Equals(GetUShortOutputSigName(index, sigNum))); + } - /// - /// Returns the Sig associated with a given SRL line index - /// and the join number of the object on the SRL subpage. - /// Note: If the join number exceeds the increment range, or the count of Sigs on the - /// list object, this will return null - /// - /// The line or item position on the SRL - /// The join number of the item on the SRL subpage - /// A Sig or null if the numbers are out of range - public StringOutputSig GetStringOutputSig(uint index, uint sigNum) - { - if (sigNum > StringIncrement) return null; - return SRL.StringOutput.FirstOrDefault(s => s.Name.Equals(GetStringOutputSigName(index, sigNum))); - } + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line or item position on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public StringOutputSig GetStringOutputSig(uint index, uint sigNum) + { + if (sigNum > StringIncrement) return null; + return SRL.StringOutput.FirstOrDefault(s => s.Name.Equals(GetStringOutputSigName(index, sigNum))); + } - /// - /// Returns the Sig associated with a given SRL line index - /// and the join number of the object on the SRL subpage. - /// Note: If the join number exceeds the increment range, or the count of Sigs on the - /// list object, this will return null - /// - /// The line on the SRL - /// The join number of the item on the SRL subpage - /// A Sig or null if the numbers are out of range - public BoolInputSig BoolInputSig(uint index, uint sigNum) - { - if (sigNum > BoolIncrement) return null; - return SRL.BooleanInput.FirstOrDefault(s => s.Name.Equals(GetBoolInputSigName(index, sigNum))); - } + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public BoolInputSig BoolInputSig(uint index, uint sigNum) + { + if (sigNum > BoolIncrement) return null; + return SRL.BooleanInput.FirstOrDefault(s => s.Name.Equals(GetBoolInputSigName(index, sigNum))); + } - /// - /// Returns the Sig associated with a given SRL line index - /// and the join number of the object on the SRL subpage. - /// Note: If the join number exceeds the increment range, or the count of Sigs on the - /// list object, this will return null - /// - /// The line on the SRL - /// The join number of the item on the SRL subpage - /// A Sig or null if the numbers are out of range - public UShortInputSig UShortInputSig(uint index, uint sigNum) - { - if (sigNum > UShortIncrement) return null; - return SRL.UShortInput.FirstOrDefault(s => s.Name.Equals(GetUShortInputSigName(index, sigNum))); - } + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public UShortInputSig UShortInputSig(uint index, uint sigNum) + { + if (sigNum > UShortIncrement) return null; + return SRL.UShortInput.FirstOrDefault(s => s.Name.Equals(GetUShortInputSigName(index, sigNum))); + } - /// - /// Returns the Sig associated with a given SRL line index - /// and the join number of the object on the SRL subpage. - /// Note: If the join number exceeds the increment range, or the count of Sigs on the - /// list object, this will return null - /// - /// The line on the SRL - /// The join number of the item on the SRL subpage - /// A Sig or null if the numbers are out of range - public StringInputSig StringInputSig(uint index, uint sigNum) - { - if (sigNum > StringIncrement) return null; - return SRL.StringInput.FirstOrDefault(s => s.Name.Equals(GetStringInputSigName(index, sigNum))); - } + /// + /// Returns the Sig associated with a given SRL line index + /// and the join number of the object on the SRL subpage. + /// Note: If the join number exceeds the increment range, or the count of Sigs on the + /// list object, this will return null + /// + /// The line on the SRL + /// The join number of the item on the SRL subpage + /// A Sig or null if the numbers are out of range + public StringInputSig StringInputSig(uint index, uint sigNum) + { + if (sigNum > StringIncrement) return null; + return SRL.StringInput.FirstOrDefault(s => s.Name.Equals(GetStringInputSigName(index, sigNum))); + } - // Helpers to get signal names + // Helpers to get signal names - string GetBoolFeedbackSigName(uint index, uint sigNum) - { - var num = (index - 1) * BoolIncrement + sigNum; - return String.Format("press{0}", num); - } + string GetBoolFeedbackSigName(uint index, uint sigNum) + { + var num = (index - 1) * BoolIncrement + sigNum; + return String.Format("press{0}", num); + } - string GetUShortOutputSigName(uint index, uint sigNum) - { - var num = (index - 1) * UShortIncrement + sigNum; - return String.Format("an_act{0}", num); - } + string GetUShortOutputSigName(uint index, uint sigNum) + { + var num = (index - 1) * UShortIncrement + sigNum; + return String.Format("an_act{0}", num); + } - string GetStringOutputSigName(uint index, uint sigNum) - { - var num = (index - 1) * StringIncrement + sigNum; - return String.Format("text-i{0}", num); - } + string GetStringOutputSigName(uint index, uint sigNum) + { + var num = (index - 1) * StringIncrement + sigNum; + return String.Format("text-i{0}", num); + } - string GetBoolInputSigName(uint index, uint sigNum) - { - var num = (index - 1) * BoolIncrement + sigNum; - return String.Format("fb{0}", num); - } + string GetBoolInputSigName(uint index, uint sigNum) + { + var num = (index - 1) * BoolIncrement + sigNum; + return String.Format("fb{0}", num); + } - string GetUShortInputSigName(uint index, uint sigNum) - { - var num = (index - 1) * UShortIncrement + sigNum; - return String.Format("an_fb{0}", num); - } + string GetUShortInputSigName(uint index, uint sigNum) + { + var num = (index - 1) * UShortIncrement + sigNum; + return String.Format("an_fb{0}", num); + } - string GetStringInputSigName(uint index, uint sigNum) - { - var num = (index - 1) * StringIncrement + sigNum; - return String.Format("text-o{0}", num); - } + string GetStringInputSigName(uint index, uint sigNum) + { + var num = (index - 1) * StringIncrement + sigNum; + return String.Format("text-o{0}", num); + } - /// - /// Stock SigChange handler - /// - /// - /// - /// - /// SRL_SigChange method - /// - public static void SRL_SigChange(GenericBase currentDevice, SmartObjectEventArgs args) - { - 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); - } + /// + /// Stock SigChange handler + /// + /// + /// + /// + /// SRL_SigChange method + /// + public static void SRL_SigChange(GenericBase currentDevice, SmartObjectEventArgs args) + { + 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); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferencList/SubpageReferenceListItem.cs b/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferencList/SubpageReferenceListItem.cs index e29d0894..878db77c 100644 --- a/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferencList/SubpageReferenceListItem.cs +++ b/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferencList/SubpageReferenceListItem.cs @@ -1,50 +1,41 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro; -using Crestron.SimplSharpPro.UI; +namespace PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core +/// +/// Represents a SubpageReferenceListItem +/// +public class SubpageReferenceListItem { - /// - /// Represents a SubpageReferenceListItem - /// - public class SubpageReferenceListItem + /// + /// The list that this lives in + /// + protected SubpageReferenceList Owner; + + /// + /// The index of this item + /// + protected uint Index; + + /// + /// Constructor + /// + /// index of the item + /// owner of the item + public SubpageReferenceListItem(uint index, SubpageReferenceList owner) { - /// - /// The list that this lives in - /// - protected SubpageReferenceList Owner; - - /// - /// The index of this item - /// - protected uint Index; - - /// - /// Constructor - /// - /// index of the item - /// owner of the item - public SubpageReferenceListItem(uint index, SubpageReferenceList owner) - { - Index = index; - Owner = owner; - } - - /// - /// Clear method - /// - /// - public virtual void Clear() - { - } - - /// - /// Refresh method - /// - public virtual void Refresh() { } + Index = index; + Owner = owner; } + + /// + /// Clear method + /// + /// + public virtual void Clear() + { + } + + /// + /// Refresh method + /// + public virtual void Refresh() { } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Timers/CountdownTimer.cs b/src/PepperDash.Essentials.Core/Timers/CountdownTimer.cs index 24ff6a1a..b44005e3 100644 --- a/src/PepperDash.Essentials.Core/Timers/CountdownTimer.cs +++ b/src/PepperDash.Essentials.Core/Timers/CountdownTimer.cs @@ -7,192 +7,152 @@ using Crestron.SimplSharp; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public class SecondsCountdownTimer: IKeyed { + public event EventHandler HasStarted; + public event EventHandler HasFinished; + public event EventHandler WasCancelled; + + public string Key { get; private set; } + + public BoolFeedback IsRunningFeedback { get; private set; } + bool _isRunning; + + public IntFeedback PercentFeedback { get; private set; } + public StringFeedback TimeRemainingFeedback { get; private set; } + + public IntFeedback SecondsRemainingFeedback { get; private set; } + + public bool CountsDown { get; set; } + /// - /// Represents a SecondsCountdownTimer + /// The number of seconds to countdown /// - public class SecondsCountdownTimer: IKeyed + public int SecondsToCount { get; set; } + + public DateTime StartTime { get; private set; } + public DateTime FinishTime { get; private set; } + + private CTimer _secondTimer; + + /// + /// Constructor + /// + /// + public SecondsCountdownTimer(string key) { - /// - /// Event fired when the timer starts - /// - public event EventHandler HasStarted; + Key = key; + IsRunningFeedback = new BoolFeedback(() => _isRunning); - /// - /// Event fired when the timer finishes - /// - public event EventHandler HasFinished; + TimeRemainingFeedback = new StringFeedback(() => + { + // Need to handle up and down here. - /// - /// Event fired when the timer is cancelled - /// - public event EventHandler WasCancelled; + var timeSpan = FinishTime - DateTime.Now; - /// - /// Gets or sets the Key - /// - public string Key { get; private set; } + Debug.LogMessage(LogEventLevel.Verbose, + "timeSpan.Minutes == {0}, timeSpan.Seconds == {1}, timeSpan.TotalSeconds == {2}", this, + timeSpan.Minutes, timeSpan.Seconds, timeSpan.TotalSeconds); - /// - /// Gets or sets the IsRunningFeedback - /// - public BoolFeedback IsRunningFeedback { get; private set; } - bool _isRunning; - - /// - /// Gets or sets the PercentFeedback - /// - public IntFeedback PercentFeedback { get; private set; } - - /// - /// Gets or sets the TimeRemainingFeedback - /// - public StringFeedback TimeRemainingFeedback { get; private set; } - - /// - /// Gets or sets the SecondsRemainingFeedback - /// - public IntFeedback SecondsRemainingFeedback { get; private set; } - - /// - /// Gets or sets the CountsDown - /// - public bool CountsDown { get; set; } - - /// - /// Gets or sets the SecondsToCount - /// - public int SecondsToCount { get; set; } - - /// - /// Gets or sets the StartTime - /// - public DateTime StartTime { get; private set; } - /// - /// Gets or sets the FinishTime - /// - public DateTime FinishTime { get; private set; } - - private CTimer _secondTimer; - - /// - /// Constructor - /// - /// - public SecondsCountdownTimer(string key) - { - Key = key; - IsRunningFeedback = new BoolFeedback(() => _isRunning); - - TimeRemainingFeedback = new StringFeedback(() => + if (Math.Floor(timeSpan.TotalSeconds) < 60 && Math.Floor(timeSpan.TotalSeconds) >= 0) //ignore milliseconds { - // Need to handle up and down here. + return String.Format("{0:00}", timeSpan.Seconds); + } - var timeSpan = FinishTime - DateTime.Now; + return Math.Floor(timeSpan.TotalSeconds) < 0 + ? "00" + : String.Format("{0:00}:{1:00}", timeSpan.Minutes, timeSpan.Seconds); + }); - Debug.LogMessage(LogEventLevel.Verbose, - "timeSpan.Minutes == {0}, timeSpan.Seconds == {1}, timeSpan.TotalSeconds == {2}", this, - timeSpan.Minutes, timeSpan.Seconds, timeSpan.TotalSeconds); + SecondsRemainingFeedback = new IntFeedback(() => (int)(FinishTime - DateTime.Now).TotalSeconds); - if (Math.Floor(timeSpan.TotalSeconds) < 60 && Math.Floor(timeSpan.TotalSeconds) >= 0) //ignore milliseconds - { - return String.Format("{0:00}", timeSpan.Seconds); - } + PercentFeedback = + new IntFeedback( + () => + (int) + (Math.Floor((FinishTime - DateTime.Now).TotalSeconds)/ + Math.Floor((FinishTime - StartTime).TotalSeconds)*100)); + } - return Math.Floor(timeSpan.TotalSeconds) < 0 - ? "00" - : String.Format("{0:00}:{1:00}", timeSpan.Minutes, timeSpan.Seconds); - }); + /// + /// Starts the Timer + /// + public void Start() + { + if (_isRunning) + return; + StartTime = DateTime.Now; + FinishTime = StartTime + TimeSpan.FromSeconds(SecondsToCount); - SecondsRemainingFeedback = new IntFeedback(() => (int)(FinishTime - DateTime.Now).TotalSeconds); + if (_secondTimer != null) + _secondTimer.Stop(); + _secondTimer = new CTimer(SecondElapsedTimerCallback, null, 0, 1000); + _isRunning = true; + IsRunningFeedback.FireUpdate(); - PercentFeedback = - new IntFeedback( - () => - (int) - (Math.Floor((FinishTime - DateTime.Now).TotalSeconds)/ - Math.Floor((FinishTime - StartTime).TotalSeconds)*100)); - } + var handler = HasStarted; + if (handler != null) + handler(this, new EventArgs()); + } - /// - /// Start method - /// - public void Start() + /// + /// Restarts the timer + /// + public void Reset() + { + _isRunning = false; + IsRunningFeedback.FireUpdate(); + Start(); + } + + /// + /// Cancels the timer (without triggering it to finish) + /// + public void Cancel() + { + StopHelper(); + + var handler = WasCancelled; + if (handler != null) + handler(this, new EventArgs()); + } + + /// + /// Called upon expiration, or calling this will force timer to finish. + /// + public void Finish() + { + StopHelper(); + + var handler = HasFinished; + if (handler != null) + handler(this, new EventArgs()); + } + + void StopHelper() + { + if (_secondTimer != null) { - if (_isRunning) - return; - StartTime = DateTime.Now; - FinishTime = StartTime + TimeSpan.FromSeconds(SecondsToCount); - - if (_secondTimer != null) - _secondTimer.Stop(); - _secondTimer = new CTimer(SecondElapsedTimerCallback, null, 0, 1000); - _isRunning = true; - IsRunningFeedback.FireUpdate(); - - var handler = HasStarted; - if (handler != null) - handler(this, new EventArgs()); + _secondTimer.Stop(); + _secondTimer = null; } - /// - /// Reset method - /// - public void Reset() + _isRunning = false; + IsRunningFeedback.FireUpdate(); + } + + void SecondElapsedTimerCallback(object o) + { + if (DateTime.Now >= FinishTime) { - _isRunning = false; - IsRunningFeedback.FireUpdate(); - Start(); + Finish(); + return; } - /// - /// Cancel method - /// - public void Cancel() - { - StopHelper(); - - var handler = WasCancelled; - if (handler != null) - handler(this, new EventArgs()); - } - - /// - /// Finish method - /// - public void Finish() - { - StopHelper(); - - var handler = HasFinished; - if (handler != null) - handler(this, new EventArgs()); - } - - void StopHelper() - { - if (_secondTimer != null) - { - _secondTimer.Stop(); - _secondTimer = null; - } - - _isRunning = false; - IsRunningFeedback.FireUpdate(); - } - - void SecondElapsedTimerCallback(object o) - { - if (DateTime.Now >= FinishTime) - { - Finish(); - return; - } - - PercentFeedback.FireUpdate(); - TimeRemainingFeedback.FireUpdate(); - SecondsRemainingFeedback.FireUpdate(); - } + PercentFeedback.FireUpdate(); + TimeRemainingFeedback.FireUpdate(); + SecondsRemainingFeedback.FireUpdate(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs b/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs index f06ba3cd..14eb3d22 100644 --- a/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs +++ b/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs @@ -1,222 +1,170 @@ - - -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using Crestron.SimplSharp; - using PepperDash.Core; -using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; - using Newtonsoft.Json; using Serilog.Events; -namespace PepperDash.Essentials.Core.Timers +namespace PepperDash.Essentials.Core.Timers; + +/// +/// A device that runs a retriggerable timer and can execute actions specified in config +/// +[Description("A retriggerable timer device")] +public class RetriggerableTimer : EssentialsDevice { - /// - /// A device that runs a retriggerable timer and can execute actions specified in config - /// - [Description("A retriggerable timer device")] - public class RetriggerableTimer : EssentialsDevice + private RetriggerableTimerPropertiesConfig _propertiesConfig; + + private CTimer _timer; + private long _timerIntervalMs; + + public RetriggerableTimer(string key, DeviceConfig config) + : base(key, config.Name) { - private RetriggerableTimerPropertiesConfig _propertiesConfig; + var props = config.Properties.ToObject(); + _propertiesConfig = props; - private CTimer _timer; - private long _timerIntervalMs; - - /// - /// Constructor - /// - /// key of the timer - /// configuration for the timer - public RetriggerableTimer(string key, DeviceConfig config) - : base(key, config.Name) + if (_propertiesConfig != null) { - var props = config.Properties.ToObject(); - _propertiesConfig = props; + _timerIntervalMs = _propertiesConfig.TimerIntervalMs; + } + } - if (_propertiesConfig != null) - { - _timerIntervalMs = _propertiesConfig.TimerIntervalMs; - } + public override bool CustomActivate() + { + if (_propertiesConfig.StartTimerOnActivation) + { + StartTimer(); } - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() + return base.CustomActivate(); + } + + private void CleanUpTimer() + { + if (_timer != null) { - if (_propertiesConfig.StartTimerOnActivation) - { - StartTimer(); - } - - return base.CustomActivate(); - } - - private void CleanUpTimer() - { - if (_timer != null) - { - _timer.Stop(); - _timer.Dispose(); - } - - _timer = null; - } - - /// - /// StartTimer method - /// - public void StartTimer() - { - CleanUpTimer(); - Debug.LogMessage(LogEventLevel.Information, this, "Starting Timer"); - - _timer = new CTimer(TimerElapsedCallback, GetActionFromConfig(eRetriggerableTimerEvents.Elapsed), _timerIntervalMs, _timerIntervalMs); - } - - /// - /// StopTimer method - /// - public void StopTimer() - { - Debug.LogMessage(LogEventLevel.Information, this, "Stopping Timer"); _timer.Stop(); - - ExecuteAction(GetActionFromConfig(eRetriggerableTimerEvents.Stopped)); + _timer.Dispose(); } - private DeviceActionWrapper GetActionFromConfig(eRetriggerableTimerEvents eventType) - { - var action = _propertiesConfig.Events[eRetriggerableTimerEvents.Elapsed]; + _timer = null; + } - if (action != null) - return action; - else return null; - } + public void StartTimer() + { + CleanUpTimer(); + Debug.LogMessage(LogEventLevel.Information, this, "Starting Timer"); - /// - /// Executes the Elapsed action from confing when the timer elapses - /// - /// action to be executed - private void TimerElapsedCallback(object action) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Timer Elapsed. Executing Action"); + _timer = new CTimer(TimerElapsedCallback, GetActionFromConfig(eRetriggerableTimerEvents.Elapsed), _timerIntervalMs, _timerIntervalMs); + } - if (action == null) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Timer elapsed but unable to execute action. Action is null."); - return; - } + public void StopTimer() + { + Debug.LogMessage(LogEventLevel.Information, this, "Stopping Timer"); + _timer.Stop(); - var devAction = action as DeviceActionWrapper; - if (devAction != null) - ExecuteAction(devAction); - else - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Unable to cast action as DeviceActionWrapper. Cannot Execute"); - } + ExecuteAction(GetActionFromConfig(eRetriggerableTimerEvents.Stopped)); + } - } + private DeviceActionWrapper GetActionFromConfig(eRetriggerableTimerEvents eventType) + { + var action = _propertiesConfig.Events[eRetriggerableTimerEvents.Elapsed]; - private void ExecuteAction(DeviceActionWrapper action) - { - if (action == null) - return; - - try - { - DeviceJsonApi.DoDeviceAction(action); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Error Executing Action: {0}", e); - } - //finally // Not sure this is needed - //{ - // _Timer.Reset(0, _TimerIntervalMs); - //} - } + if (action != null) + return action; + else return null; } /// - /// Configuration Properties for RetriggerableTimer + /// Executes the Elapsed action from confing when the timer elapses /// - public class RetriggerableTimerPropertiesConfig + /// + private void TimerElapsedCallback(object action) { - /// - /// Start the timer on device activation - /// - [JsonProperty("startTimerOnActivation")] - public bool StartTimerOnActivation { get; set; } + Debug.LogMessage(LogEventLevel.Debug, this, "Timer Elapsed. Executing Action"); - /// - /// Timer interval in milliseconds - /// - [JsonProperty("timerIntervalMs")] - public long TimerIntervalMs { get; set; } - - /// - /// Events and their associated actions - /// - [JsonProperty("events")] - public Dictionary Events { get; set; } - - /// - /// Constructor - /// - public RetriggerableTimerPropertiesConfig() + if (action == null) { - Events = new Dictionary(); - } - } - - /// - /// Enumeration of eRetriggerableTimerEvents values - /// - public enum eRetriggerableTimerEvents - { - /// - /// Elapsed event - /// - Elapsed, - - /// - /// Stopped event - /// - Stopped, - } - - /// - /// Factory class - /// - public class RetriggerableTimerFactory : EssentialsDeviceFactory - { - /// - /// Constructor - /// - public RetriggerableTimerFactory() - { - TypeNames = new List() { "retriggerabletimer" }; + Debug.LogMessage(LogEventLevel.Debug, this, "Timer elapsed but unable to execute action. Action is null."); + return; } - /// - /// BuildDevice method - /// - /// device config - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) + var devAction = action as DeviceActionWrapper; + if (devAction != null) + ExecuteAction(devAction); + else { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new RetriggerableTimer Device"); - - return new RetriggerableTimer(dc.Key, dc); + Debug.LogMessage(LogEventLevel.Verbose, this, "Unable to cast action as DeviceActionWrapper. Cannot Execute"); } + } + private void ExecuteAction(DeviceActionWrapper action) + { + if (action == null) + return; + try + { + DeviceJsonApi.DoDeviceAction(action); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Error Executing Action: {0}", e); + } + //finally // Not sure this is needed + //{ + // _Timer.Reset(0, _TimerIntervalMs); + //} + } +} + +/// +/// Configuration Properties for RetriggerableTimer +/// +public class RetriggerableTimerPropertiesConfig +{ + [JsonProperty("startTimerOnActivation")] + public bool StartTimerOnActivation { get; set; } + + [JsonProperty("timerIntervalMs")] + public long TimerIntervalMs { get; set; } + + [JsonProperty("events")] + public Dictionary Events { get; set; } + + public RetriggerableTimerPropertiesConfig() + { + Events = new Dictionary(); + } +} + +/// +/// The set of values describing events on the timer +/// +public enum eRetriggerableTimerEvents +{ + Elapsed, + Stopped, +} + +/// +/// Factory class +/// +public class RetriggerableTimerFactory : EssentialsDeviceFactory +{ + public RetriggerableTimerFactory() + { + TypeNames = new List() { "retriggerabletimer" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new RetriggerableTimer Device"); + + return new RetriggerableTimer(dc.Key, dc); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Touchpanels/CrestronTouchpanelPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Touchpanels/CrestronTouchpanelPropertiesConfig.cs index 2cc9ab9a..478869b3 100644 --- a/src/PepperDash.Essentials.Core/Touchpanels/CrestronTouchpanelPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Touchpanels/CrestronTouchpanelPropertiesConfig.cs @@ -1,149 +1,90 @@ using Newtonsoft.Json; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public class CrestronTouchpanelPropertiesConfig { - /// - /// Represents a CrestronTouchpanelPropertiesConfig - /// - public class CrestronTouchpanelPropertiesConfig - { - /// - /// Gets or sets the ControlProperties - /// - [JsonProperty("control")] - public EssentialsControlPropertiesConfig ControlProperties { get; set; } + [JsonProperty("control")] + public EssentialsControlPropertiesConfig ControlProperties { get; set; } - /// - /// Gets or sets the IpId - /// - [JsonProperty("ipId", NullValueHandling = NullValueHandling.Ignore)] - public string IpId { get; set; } + [JsonProperty("ipId", NullValueHandling = NullValueHandling.Ignore)] + public string IpId { get; set; } - /// - /// Gets or sets the DefaultRoomKey - /// - [JsonProperty("defaultRoomKey", NullValueHandling = NullValueHandling.Ignore)] - public string DefaultRoomKey { get; set; } - - /// - /// Gets or sets the RoomListKey - /// - [JsonProperty("roomListKey", NullValueHandling = NullValueHandling.Ignore)] - public string RoomListKey { get; set; } + [JsonProperty("defaultRoomKey", NullValueHandling = NullValueHandling.Ignore)] + public string DefaultRoomKey { get; set; } + + [JsonProperty("roomListKey", NullValueHandling = NullValueHandling.Ignore)] + public string RoomListKey { get; set; } - /// - /// Gets or sets the SgdFile - /// - [JsonProperty("sgdFile", NullValueHandling = NullValueHandling.Ignore)] - public string SgdFile { get; set; } + [JsonProperty("sgdFile", NullValueHandling = NullValueHandling.Ignore)] + public string SgdFile { get; set; } - /// - /// Gets or sets the ProjectName - /// - [JsonProperty("projectName", NullValueHandling = NullValueHandling.Ignore)] - public string ProjectName { get; set; } + [JsonProperty("projectName", NullValueHandling = NullValueHandling.Ignore)] + public string ProjectName { get; set; } - /// - /// Gets or sets the ShowVolumeGauge - /// - [JsonProperty("showVolumeGauge", NullValueHandling = NullValueHandling.Ignore)] - public bool? ShowVolumeGauge { get; set; } + [JsonProperty("showVolumeGauge", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowVolumeGauge { get; set; } - /// - /// Gets or sets the UsesSplashPage - /// - [JsonProperty("usesSplashPage", NullValueHandling = NullValueHandling.Ignore)] - public bool? UsesSplashPage { get; set; } + [JsonProperty("usesSplashPage", NullValueHandling = NullValueHandling.Ignore)] + public bool? UsesSplashPage { get; set; } - /// - /// Gets or sets the ShowDate - /// - [JsonProperty("showDate", NullValueHandling = NullValueHandling.Ignore)] - public bool? ShowDate { get; set; } + [JsonProperty("showDate", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowDate { get; set; } - /// - /// Gets or sets the ShowTime - /// - [JsonProperty("showTime", NullValueHandling = NullValueHandling.Ignore)] - public bool? ShowTime { get; set; } + [JsonProperty("showTime", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowTime { get; set; } - /// - /// Gets or sets the Setup - /// - [JsonProperty("setup", NullValueHandling = NullValueHandling.Ignore)] - public UiSetupPropertiesConfig Setup { get; set; } + [JsonProperty("setup", NullValueHandling = NullValueHandling.Ignore)] + public UiSetupPropertiesConfig Setup { get; set; } - /// - /// Gets or sets the HeaderStyle - /// - [JsonProperty("headerStyle", NullValueHandling = NullValueHandling.Ignore)] - public string HeaderStyle { get; set; } + [JsonProperty("headerStyle", NullValueHandling = NullValueHandling.Ignore)] + public string HeaderStyle { get; set; } - /// - /// Gets or sets the IncludeInFusionRoomHealth - /// - [JsonProperty("includeInFusionRoomHealth", NullValueHandling = NullValueHandling.Ignore)] - public bool? IncludeInFusionRoomHealth { get; set; } + [JsonProperty("includeInFusionRoomHealth", NullValueHandling = NullValueHandling.Ignore)] + public bool? IncludeInFusionRoomHealth { get; set; } - /// - /// Gets or sets the ScreenSaverTimeoutMin - /// - [JsonProperty("screenSaverTimeoutMin", NullValueHandling = NullValueHandling.Ignore)] - public uint? ScreenSaverTimeoutMin { get; set; } + [JsonProperty("screenSaverTimeoutMin", NullValueHandling = NullValueHandling.Ignore)] + public uint? ScreenSaverTimeoutMin { get; set; } - /// - /// Gets or sets the ScreenSaverMovePositionIntervalMs - /// - [JsonProperty("screenSaverMovePositionIntervalMs", NullValueHandling = NullValueHandling.Ignore)] - public uint? ScreenSaverMovePositionIntervalMs { get; set; } + [JsonProperty("screenSaverMovePositionIntervalMs", NullValueHandling = NullValueHandling.Ignore)] + public uint? ScreenSaverMovePositionIntervalMs { get; set; } - /// - /// The count of sources that will trigger the "additional" arrows to show on the SRL. - /// Defaults to 5 - /// - [JsonProperty("sourcesOverflowCount", NullValueHandling = NullValueHandling.Ignore)] - public int? SourcesOverflowCount { get; set; } - - /// - /// Constructor - /// - public CrestronTouchpanelPropertiesConfig() : this(false) { } - - /// - /// Constructor - /// - /// set values to default if true - public CrestronTouchpanelPropertiesConfig(bool setDefaultValues = false) - { - if(!setDefaultValues) { return; } - SourcesOverflowCount = 5; - HeaderStyle = Habanero; - - // Default values - ScreenSaverTimeoutMin = 5; - ScreenSaverMovePositionIntervalMs = 15000; - } - - /// - /// "habanero" - /// - public const string Habanero = "habanero"; - /// - /// "verbose" - /// - public const string Verbose = "verbose"; - } - /// - /// Represents a UiSetupPropertiesConfig + /// The count of sources that will trigger the "additional" arrows to show on the SRL. + /// Defaults to 5 /// - public class UiSetupPropertiesConfig + [JsonProperty("sourcesOverflowCount", NullValueHandling = NullValueHandling.Ignore)] + public int? SourcesOverflowCount { get; set; } + + public CrestronTouchpanelPropertiesConfig() : this(false) { } + + public CrestronTouchpanelPropertiesConfig(bool setDefaultValues = false) { - /// - /// Gets or sets the IsVisible - /// - [JsonProperty("isVisible", NullValueHandling = NullValueHandling.Ignore)] - public bool IsVisible { get; set; } - } + if(!setDefaultValues) { return; } + SourcesOverflowCount = 5; + HeaderStyle = Habanero; + + // Default values + ScreenSaverTimeoutMin = 5; + ScreenSaverMovePositionIntervalMs = 15000; + } + + /// + /// "habanero" + /// + public const string Habanero = "habanero"; + /// + /// "verbose" + /// + public const string Verbose = "verbose"; +} + +/// +/// +/// +public class UiSetupPropertiesConfig +{ + [JsonProperty("isVisible", NullValueHandling = NullValueHandling.Ignore)] + public bool IsVisible { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Touchpanels/Interfaces.cs b/src/PepperDash.Essentials.Core/Touchpanels/Interfaces.cs index 0e74ed93..0d18f133 100644 --- a/src/PepperDash.Essentials.Core/Touchpanels/Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Touchpanels/Interfaces.cs @@ -1,20 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Defines the contract for IHasBasicTriListWithSmartObject +/// +public interface IHasBasicTriListWithSmartObject { /// - /// Defines the contract for IHasBasicTriListWithSmartObject + /// Gets the Panel /// - public interface IHasBasicTriListWithSmartObject - { - /// - /// Gets the Panel - /// - BasicTriListWithSmartObject Panel { get; } - } -} \ No newline at end of file + BasicTriListWithSmartObject Panel { get; } +} diff --git a/src/PepperDash.Essentials.Core/Touchpanels/Keyboards/HabaneroKeyboardController.cs b/src/PepperDash.Essentials.Core/Touchpanels/Keyboards/HabaneroKeyboardController.cs index 89346ae7..ea3dd372 100644 --- a/src/PepperDash.Essentials.Core/Touchpanels/Keyboards/HabaneroKeyboardController.cs +++ b/src/PepperDash.Essentials.Core/Touchpanels/Keyboards/HabaneroKeyboardController.cs @@ -1,247 +1,209 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro.DeviceSupport; -namespace PepperDash.Essentials.Core.Touchpanels.Keyboards +namespace PepperDash.Essentials.Core.Touchpanels.Keyboards; + +public class HabaneroKeyboardController { /// - /// Represents a HabaneroKeyboardController + /// Single-key press events, rather than using a built-up text string on the OutputFeedback /// - public class HabaneroKeyboardController - { - /// - /// Single-key press events, rather than using a built-up text string on the OutputFeedback - /// - public event EventHandler KeyPress; + public event EventHandler KeyPress; - /// - /// Gets or sets the TriList - /// - public BasicTriList TriList { get; private set; } + public BasicTriList TriList { get; private set; } - /// - /// Gets or sets the OutputFeedback - /// - public StringFeedback OutputFeedback { get; private set; } + public StringFeedback OutputFeedback { get; private set; } - /// - /// Gets or sets the IsVisible - /// - public bool IsVisible { get; private set; } + public bool IsVisible { get; private set; } - /// - /// Gets or sets the DotComButtonString - /// - public string DotComButtonString { get; set; } + public string DotComButtonString { get; set; } - /// - /// Gets or sets the GoButtonText - /// - public string GoButtonText { get; set; } + public string GoButtonText { get; set; } - /// - /// Gets or sets the SecondaryButtonText - /// - public string SecondaryButtonText { get; set; } + public string SecondaryButtonText { get; set; } - /// - /// Gets or sets the GoButtonVisible - /// - public bool GoButtonVisible { get; set; } + public bool GoButtonVisible { get; set; } - /// - /// Gets or sets the SecondaryButtonVisible - /// - public bool SecondaryButtonVisible { get; set; } + public bool SecondaryButtonVisible { get; set; } - int ShiftMode = 0; - - StringBuilder Output; + int ShiftMode = 0; + + StringBuilder Output; - /// - /// Gets or sets the HideAction - /// - public Action HideAction { get; set; } + public Action HideAction { get; set; } CTimer BackspaceTimer; - /// - /// - /// - /// - public HabaneroKeyboardController(BasicTriList trilist) - { - TriList = trilist; - Output = new StringBuilder(); - OutputFeedback = new StringFeedback(() => Output.ToString()); - DotComButtonString = ".com"; - } + /// + /// + /// + /// + public HabaneroKeyboardController(BasicTriList trilist) + { + TriList = trilist; + Output = new StringBuilder(); + OutputFeedback = new StringFeedback(() => Output.ToString()); + DotComButtonString = ".com"; + } - /// - /// Show method - /// - public void Show() - { - if (IsVisible) - return; + /// + /// Shows the keyboard and attaches sig handlers in the range of 2901-2969 + /// + public void Show() + { + if (IsVisible) + return; - TriList.SetSigTrueAction(ClosePressJoin, Hide); - TriList.SetSigTrueAction(GoButtonPressJoin, () => OnKeyPress(KeyboardSpecialKey.GoButton)); - TriList.SetSigTrueAction(SecondaryButtonPressJoin, () => OnKeyPress(KeyboardSpecialKey.SecondaryButton)); - TriList.SetSigTrueAction(2921, () => Press(A(ShiftMode))); - TriList.SetSigTrueAction(2922, () => Press(B(ShiftMode))); - TriList.SetSigTrueAction(2923, () => Press(C(ShiftMode))); - TriList.SetSigTrueAction(2924, () => Press(D(ShiftMode))); - TriList.SetSigTrueAction(2925, () => Press(E(ShiftMode))); - TriList.SetSigTrueAction(2926, () => Press(F(ShiftMode))); - TriList.SetSigTrueAction(2927, () => Press(G(ShiftMode))); - TriList.SetSigTrueAction(2928, () => Press(H(ShiftMode))); - TriList.SetSigTrueAction(2929, () => Press(I(ShiftMode))); - TriList.SetSigTrueAction(2930, () => Press(J(ShiftMode))); - TriList.SetSigTrueAction(2931, () => Press(K(ShiftMode))); - TriList.SetSigTrueAction(2932, () => Press(L(ShiftMode))); - TriList.SetSigTrueAction(2933, () => Press(M(ShiftMode))); - TriList.SetSigTrueAction(2934, () => Press(N(ShiftMode))); - TriList.SetSigTrueAction(2935, () => Press(O(ShiftMode))); - TriList.SetSigTrueAction(2936, () => Press(P(ShiftMode))); - TriList.SetSigTrueAction(2937, () => Press(Q(ShiftMode))); - TriList.SetSigTrueAction(2938, () => Press(R(ShiftMode))); - TriList.SetSigTrueAction(2939, () => Press(S(ShiftMode))); - TriList.SetSigTrueAction(2940, () => Press(T(ShiftMode))); - TriList.SetSigTrueAction(2941, () => Press(U(ShiftMode))); - TriList.SetSigTrueAction(2942, () => Press(V(ShiftMode))); - TriList.SetSigTrueAction(2943, () => Press(W(ShiftMode))); - TriList.SetSigTrueAction(2944, () => Press(X(ShiftMode))); - TriList.SetSigTrueAction(2945, () => Press(Y(ShiftMode))); - TriList.SetSigTrueAction(2946, () => Press(Z(ShiftMode))); - TriList.SetSigTrueAction(2947, () => Press('.')); - TriList.SetSigTrueAction(2948, () => Press('@')); - TriList.SetSigTrueAction(2949, () => Press(' ')); + TriList.SetSigTrueAction(ClosePressJoin, Hide); + TriList.SetSigTrueAction(GoButtonPressJoin, () => OnKeyPress(KeyboardSpecialKey.GoButton)); + TriList.SetSigTrueAction(SecondaryButtonPressJoin, () => OnKeyPress(KeyboardSpecialKey.SecondaryButton)); + TriList.SetSigTrueAction(2921, () => Press(A(ShiftMode))); + TriList.SetSigTrueAction(2922, () => Press(B(ShiftMode))); + TriList.SetSigTrueAction(2923, () => Press(C(ShiftMode))); + TriList.SetSigTrueAction(2924, () => Press(D(ShiftMode))); + TriList.SetSigTrueAction(2925, () => Press(E(ShiftMode))); + TriList.SetSigTrueAction(2926, () => Press(F(ShiftMode))); + TriList.SetSigTrueAction(2927, () => Press(G(ShiftMode))); + TriList.SetSigTrueAction(2928, () => Press(H(ShiftMode))); + TriList.SetSigTrueAction(2929, () => Press(I(ShiftMode))); + TriList.SetSigTrueAction(2930, () => Press(J(ShiftMode))); + TriList.SetSigTrueAction(2931, () => Press(K(ShiftMode))); + TriList.SetSigTrueAction(2932, () => Press(L(ShiftMode))); + TriList.SetSigTrueAction(2933, () => Press(M(ShiftMode))); + TriList.SetSigTrueAction(2934, () => Press(N(ShiftMode))); + TriList.SetSigTrueAction(2935, () => Press(O(ShiftMode))); + TriList.SetSigTrueAction(2936, () => Press(P(ShiftMode))); + TriList.SetSigTrueAction(2937, () => Press(Q(ShiftMode))); + TriList.SetSigTrueAction(2938, () => Press(R(ShiftMode))); + TriList.SetSigTrueAction(2939, () => Press(S(ShiftMode))); + TriList.SetSigTrueAction(2940, () => Press(T(ShiftMode))); + TriList.SetSigTrueAction(2941, () => Press(U(ShiftMode))); + TriList.SetSigTrueAction(2942, () => Press(V(ShiftMode))); + TriList.SetSigTrueAction(2943, () => Press(W(ShiftMode))); + TriList.SetSigTrueAction(2944, () => Press(X(ShiftMode))); + TriList.SetSigTrueAction(2945, () => Press(Y(ShiftMode))); + TriList.SetSigTrueAction(2946, () => Press(Z(ShiftMode))); + TriList.SetSigTrueAction(2947, () => Press('.')); + TriList.SetSigTrueAction(2948, () => Press('@')); + TriList.SetSigTrueAction(2949, () => Press(' ')); TriList.SetSigHeldAction(2950, 500, StartBackspaceRepeat, StopBackspaceRepeat, Backspace); //TriList.SetSigTrueAction(2950, Backspace); - TriList.SetSigTrueAction(2951, Shift); - TriList.SetSigTrueAction(2952, NumShift); - TriList.SetSigTrueAction(2953, Clear); - TriList.SetSigTrueAction(2954, () => Press(DotComButtonString)); + TriList.SetSigTrueAction(2951, Shift); + TriList.SetSigTrueAction(2952, NumShift); + TriList.SetSigTrueAction(2953, Clear); + TriList.SetSigTrueAction(2954, () => Press(DotComButtonString)); - TriList.SetBool(GoButtonVisibleJoin, GoButtonVisible); - TriList.SetString(GoButtonTextJoin, GoButtonText); - TriList.SetBool(SecondaryButtonVisibleJoin, SecondaryButtonVisible); - TriList.SetString(SecondaryButtonTextJoin, SecondaryButtonText); + TriList.SetBool(GoButtonVisibleJoin, GoButtonVisible); + TriList.SetString(GoButtonTextJoin, GoButtonText); + TriList.SetBool(SecondaryButtonVisibleJoin, SecondaryButtonVisible); + TriList.SetString(SecondaryButtonTextJoin, SecondaryButtonText); - TriList.SetBool(KeyboardVisible, true); + TriList.SetBool(KeyboardVisible, true); + ShowKeys(); + IsVisible = true; + } + + /// + /// Hides the keyboard and disconnects ALL sig handlers from 2901 - 2969 + /// + public void Hide() + { + if (!IsVisible) + return; + + for (uint i = 2901; i < 2970; i++) + TriList.ClearBoolSigAction(i); + + // run attached actions + if(HideAction != null) + HideAction(); + + TriList.SetBool(KeyboardVisible, false); + IsVisible = false; + } + + /// + /// + /// + /// + public void Press(char c) + { + OnKeyPress(c.ToString()); + Output.Append(c); + OutputFeedback.FireUpdate(); + ResetShift(); + } + + /// + /// + /// + /// + public void Press(string s) + { + OnKeyPress(s); + Output.Append(s); + OutputFeedback.FireUpdate(); + ResetShift(); + } + + /// + /// + /// + public void EnableGoButton() + { + TriList.SetBool(GoButtonEnableJoin, true); + } + + /// + /// + /// + public void DisableGoButton() + { + TriList.SetBool(GoButtonEnableJoin, false); + } + + void ResetShift() + { + if (ShiftMode == 1) + { + ShiftMode = 0; ShowKeys(); - IsVisible = true; } - - /// - /// Hide method - /// - public void Hide() + else if (ShiftMode == 3) { - if (!IsVisible) - return; - - for (uint i = 2901; i < 2970; i++) - TriList.ClearBoolSigAction(i); - - // run attached actions - if(HideAction != null) - HideAction(); - - TriList.SetBool(KeyboardVisible, false); - IsVisible = false; + ShiftMode = 2; + ShowKeys(); } + } - /// - /// - /// - /// - /// - /// Press method - /// - public void Press(char c) - { - OnKeyPress(c.ToString()); - Output.Append(c); - OutputFeedback.FireUpdate(); - ResetShift(); - } - - /// - /// - /// - /// - /// - /// Press method - /// - public void Press(string s) - { - OnKeyPress(s); - Output.Append(s); - OutputFeedback.FireUpdate(); - ResetShift(); - } - - /// - /// EnableGoButton method - /// - public void EnableGoButton() - { - TriList.SetBool(GoButtonEnableJoin, true); - } - - /// - /// - /// - public void DisableGoButton() - { - TriList.SetBool(GoButtonEnableJoin, false); - } - - void ResetShift() - { - if (ShiftMode == 1) - { - ShiftMode = 0; - ShowKeys(); - } - else if (ShiftMode == 3) - { - ShiftMode = 2; - ShowKeys(); - } - } - - char A(int i) { return new char[] { 'a', 'A', '?', '?' }[i]; } - char B(int i) { return new char[] { 'b', 'B', ':', ':' }[i]; } - char C(int i) { return new char[] { 'c', 'C', '>', '>' }[i]; } - char D(int i) { return new char[] { 'd', 'D', '_', '_' }[i]; } - char E(int i) { return new char[] { 'e', 'E', '3', '#' }[i]; } - char F(int i) { return new char[] { 'f', 'F', '=', '=' }[i]; } - char G(int i) { return new char[] { 'g', 'G', '+', '+' }[i]; } - char H(int i) { return new char[] { 'h', 'H', '[', '[' }[i]; } - char I(int i) { return new char[] { 'i', 'I', '8', '*' }[i]; } - char J(int i) { return new char[] { 'j', 'J', ']', ']' }[i]; } - char K(int i) { return new char[] { 'k', 'K', '/', '/' }[i]; } - char L(int i) { return new char[] { 'l', 'L', '\\', '\\' }[i]; } - char M(int i) { return new char[] { 'm', 'M', '"', '"' }[i]; } - char N(int i) { return new char[] { 'n', 'N', '\'', '\'' }[i]; } - char O(int i) { return new char[] { 'o', 'O', '9', '(' }[i]; } - char P(int i) { return new char[] { 'p', 'P', '0', ')' }[i]; } - char Q(int i) { return new char[] { 'q', 'Q', '1', '!' }[i]; } - char R(int i) { return new char[] { 'r', 'R', '4', '$' }[i]; } - char S(int i) { return new char[] { 's', 'S', '-', '-' }[i]; } - char T(int i) { return new char[] { 't', 'T', '5', '%' }[i]; } - char U(int i) { return new char[] { 'u', 'U', '7', '&' }[i]; } - char V(int i) { return new char[] { 'v', 'V', ';', ';' }[i]; } - char W(int i) { return new char[] { 'w', 'W', '2', '@' }[i]; } - char X(int i) { return new char[] { 'x', 'X', '<', '<' }[i]; } - char Y(int i) { return new char[] { 'y', 'Y', '6', '^' }[i]; } - char Z(int i) { return new char[] { 'z', 'Z', ',', ',' }[i]; } + char A(int i) { return new char[] { 'a', 'A', '?', '?' }[i]; } + char B(int i) { return new char[] { 'b', 'B', ':', ':' }[i]; } + char C(int i) { return new char[] { 'c', 'C', '>', '>' }[i]; } + char D(int i) { return new char[] { 'd', 'D', '_', '_' }[i]; } + char E(int i) { return new char[] { 'e', 'E', '3', '#' }[i]; } + char F(int i) { return new char[] { 'f', 'F', '=', '=' }[i]; } + char G(int i) { return new char[] { 'g', 'G', '+', '+' }[i]; } + char H(int i) { return new char[] { 'h', 'H', '[', '[' }[i]; } + char I(int i) { return new char[] { 'i', 'I', '8', '*' }[i]; } + char J(int i) { return new char[] { 'j', 'J', ']', ']' }[i]; } + char K(int i) { return new char[] { 'k', 'K', '/', '/' }[i]; } + char L(int i) { return new char[] { 'l', 'L', '\\', '\\' }[i]; } + char M(int i) { return new char[] { 'm', 'M', '"', '"' }[i]; } + char N(int i) { return new char[] { 'n', 'N', '\'', '\'' }[i]; } + char O(int i) { return new char[] { 'o', 'O', '9', '(' }[i]; } + char P(int i) { return new char[] { 'p', 'P', '0', ')' }[i]; } + char Q(int i) { return new char[] { 'q', 'Q', '1', '!' }[i]; } + char R(int i) { return new char[] { 'r', 'R', '4', '$' }[i]; } + char S(int i) { return new char[] { 's', 'S', '-', '-' }[i]; } + char T(int i) { return new char[] { 't', 'T', '5', '%' }[i]; } + char U(int i) { return new char[] { 'u', 'U', '7', '&' }[i]; } + char V(int i) { return new char[] { 'v', 'V', ';', ';' }[i]; } + char W(int i) { return new char[] { 'w', 'W', '2', '@' }[i]; } + char X(int i) { return new char[] { 'x', 'X', '<', '<' }[i]; } + char Y(int i) { return new char[] { 'y', 'Y', '6', '^' }[i]; } + char Z(int i) { return new char[] { 'z', 'Z', ',', ',' }[i]; } /// /// Does what it says @@ -266,248 +228,206 @@ namespace PepperDash.Essentials.Core.Touchpanels.Keyboards } } - void Backspace() + void Backspace() + { + OnKeyPress(KeyboardSpecialKey.Backspace); + + if (Output.Length > 0) { - OnKeyPress(KeyboardSpecialKey.Backspace); - - if (Output.Length > 0) - { - Output.Remove(Output.Length - 1, 1); - OutputFeedback.FireUpdate(); - } - } - - void Clear() - { - OnKeyPress(KeyboardSpecialKey.Clear); - - Output.Remove(0, Output.Length); + Output.Remove(Output.Length - 1, 1); OutputFeedback.FireUpdate(); } + } - /* When in mode 0 (lowercase): - * shift button: up arrow 0 - * numShift button: 123/#$@#$ 0 - * - * - shift --> mode 1 - * - double-tap shift --> caps lock - * - numShift --> mode 2 - * - * mode 1 (uppercase) - * shift button: down arrow 1 - * numShift button: 123/##$# 0 - * - * - shift --> mode 0 - * - numShift --> mode 2 - * - * - Tapping any key will go back to mode 0 - * - * mode 2 (numbers-sym) - * Shift button: #$#$#$ 2 - * numShift: ABC 1 - * - * - shift --> mode 3 - * - double-tap shift --> caps lock - * - numShift --> mode 0 - * - * mode 3 (sym) - * Shift button: 123 3 - * numShift: ABC 1 - * - * - shift --> mode 2 - * - numShift --> mode 0 - * - * - Tapping any key will go back to mode 2 - */ - void Shift() - { - if (ShiftMode == 0) - ShiftMode = 1; - else if (ShiftMode == 1) - ShiftMode = 0; - else if (ShiftMode == 2) - ShiftMode = 3; - else - ShiftMode = 2; + void Clear() + { + OnKeyPress(KeyboardSpecialKey.Clear); - ShowKeys(); - } + Output.Remove(0, Output.Length); + OutputFeedback.FireUpdate(); + } - void NumShift() - { - if (ShiftMode == 0 || ShiftMode == 1) - ShiftMode = 2; - else if (ShiftMode == 2 || ShiftMode == 3) - ShiftMode = 0; - ShowKeys(); - } + /* When in mode 0 (lowercase): + * shift button: up arrow 0 + * numShift button: 123/#$@#$ 0 + * + * - shift --> mode 1 + * - double-tap shift --> caps lock + * - numShift --> mode 2 + * + * mode 1 (uppercase) + * shift button: down arrow 1 + * numShift button: 123/##$# 0 + * + * - shift --> mode 0 + * - numShift --> mode 2 + * + * - Tapping any key will go back to mode 0 + * + * mode 2 (numbers-sym) + * Shift button: #$#$#$ 2 + * numShift: ABC 1 + * + * - shift --> mode 3 + * - double-tap shift --> caps lock + * - numShift --> mode 0 + * + * mode 3 (sym) + * Shift button: 123 3 + * numShift: ABC 1 + * + * - shift --> mode 2 + * - numShift --> mode 0 + * + * - Tapping any key will go back to mode 2 + */ + void Shift() + { + if (ShiftMode == 0) + ShiftMode = 1; + else if (ShiftMode == 1) + ShiftMode = 0; + else if (ShiftMode == 2) + ShiftMode = 3; + else + ShiftMode = 2; - void ShowKeys() - { - TriList.SetString(2921, A(ShiftMode).ToString()); - TriList.SetString(2922, B(ShiftMode).ToString()); - TriList.SetString(2923, C(ShiftMode).ToString()); - TriList.SetString(2924, D(ShiftMode).ToString()); - TriList.SetString(2925, E(ShiftMode).ToString()); - TriList.SetString(2926, F(ShiftMode).ToString()); - TriList.SetString(2927, G(ShiftMode).ToString()); - TriList.SetString(2928, H(ShiftMode).ToString()); - TriList.SetString(2929, I(ShiftMode).ToString()); - TriList.SetString(2930, J(ShiftMode).ToString()); - TriList.SetString(2931, K(ShiftMode).ToString()); - TriList.SetString(2932, L(ShiftMode).ToString()); - TriList.SetString(2933, M(ShiftMode).ToString()); - TriList.SetString(2934, N(ShiftMode).ToString()); - TriList.SetString(2935, O(ShiftMode).ToString()); - TriList.SetString(2936, P(ShiftMode).ToString()); - TriList.SetString(2937, Q(ShiftMode).ToString()); - TriList.SetString(2938, R(ShiftMode).ToString()); - TriList.SetString(2939, S(ShiftMode).ToString()); - TriList.SetString(2940, T(ShiftMode).ToString()); - TriList.SetString(2941, U(ShiftMode).ToString()); - TriList.SetString(2942, V(ShiftMode).ToString()); - TriList.SetString(2943, W(ShiftMode).ToString()); - TriList.SetString(2944, X(ShiftMode).ToString()); - TriList.SetString(2945, Y(ShiftMode).ToString()); - TriList.SetString(2946, Z(ShiftMode).ToString()); - TriList.SetString(2954, DotComButtonString); + ShowKeys(); + } - TriList.SetUshort(2951, (ushort)ShiftMode); // 0 = up, 1 = down, 2 = #, 3 = 123 - TriList.SetUshort(2952, (ushort)(ShiftMode < 2 ? 0 : 1)); // 0 = #, 1 = abc - } + void NumShift() + { + if (ShiftMode == 0 || ShiftMode == 1) + ShiftMode = 2; + else if (ShiftMode == 2 || ShiftMode == 3) + ShiftMode = 0; + ShowKeys(); + } - /// - /// Event fire helper for text - /// - /// - void OnKeyPress(string text) - { - var handler = KeyPress; - if (handler != null) - KeyPress(this, new KeyboardControllerPressEventArgs(text)); - } - - /// - /// event helper for special keys - /// - /// - void OnKeyPress(KeyboardSpecialKey key) - { - var handler = KeyPress; - if (handler != null) - KeyPress(this, new KeyboardControllerPressEventArgs(key)); - } - - - /// - /// 2901 - /// - public const uint KeyboardVisible = 2901; - /// - /// 2902 - /// - public const uint ClosePressJoin = 2902; - /// - /// 2903 - /// - public const uint GoButtonPressJoin = 2903; - /// - /// 2903 - /// - public const uint GoButtonTextJoin = 2903; - /// - /// 2904 - /// - public const uint SecondaryButtonPressJoin = 2904; - /// - /// 2904 - /// - public const uint SecondaryButtonTextJoin = 2904; - /// - /// 2905 - /// - public const uint GoButtonVisibleJoin = 2905; - /// - /// 2906 - /// - public const uint SecondaryButtonVisibleJoin = 2906; - /// - /// 2907 - /// - public const uint GoButtonEnableJoin = 2907; - /// - /// 2910 - /// - public const uint ClearPressJoin = 2910; - /// - /// 2911 - /// - public const uint ClearVisibleJoin = 2911; + void ShowKeys() + { + TriList.SetString(2921, A(ShiftMode).ToString()); + TriList.SetString(2922, B(ShiftMode).ToString()); + TriList.SetString(2923, C(ShiftMode).ToString()); + TriList.SetString(2924, D(ShiftMode).ToString()); + TriList.SetString(2925, E(ShiftMode).ToString()); + TriList.SetString(2926, F(ShiftMode).ToString()); + TriList.SetString(2927, G(ShiftMode).ToString()); + TriList.SetString(2928, H(ShiftMode).ToString()); + TriList.SetString(2929, I(ShiftMode).ToString()); + TriList.SetString(2930, J(ShiftMode).ToString()); + TriList.SetString(2931, K(ShiftMode).ToString()); + TriList.SetString(2932, L(ShiftMode).ToString()); + TriList.SetString(2933, M(ShiftMode).ToString()); + TriList.SetString(2934, N(ShiftMode).ToString()); + TriList.SetString(2935, O(ShiftMode).ToString()); + TriList.SetString(2936, P(ShiftMode).ToString()); + TriList.SetString(2937, Q(ShiftMode).ToString()); + TriList.SetString(2938, R(ShiftMode).ToString()); + TriList.SetString(2939, S(ShiftMode).ToString()); + TriList.SetString(2940, T(ShiftMode).ToString()); + TriList.SetString(2941, U(ShiftMode).ToString()); + TriList.SetString(2942, V(ShiftMode).ToString()); + TriList.SetString(2943, W(ShiftMode).ToString()); + TriList.SetString(2944, X(ShiftMode).ToString()); + TriList.SetString(2945, Y(ShiftMode).ToString()); + TriList.SetString(2946, Z(ShiftMode).ToString()); + TriList.SetString(2954, DotComButtonString); + TriList.SetUshort(2951, (ushort)ShiftMode); // 0 = up, 1 = down, 2 = #, 3 = 123 + TriList.SetUshort(2952, (ushort)(ShiftMode < 2 ? 0 : 1)); // 0 = #, 1 = abc } /// - /// Event args for keyboard key presses + /// Event fire helper for text /// - public class KeyboardControllerPressEventArgs : EventArgs + /// + void OnKeyPress(string text) { - /// - /// Gets or sets the Text - /// - public string Text { get; private set; } - - /// - /// Gets or sets the SpecialKey - /// - public KeyboardSpecialKey SpecialKey { get; private set; } - - /// - /// Constructor - /// - /// - public KeyboardControllerPressEventArgs(string text) - { - Text = text; - } - - /// - /// Constructor - /// - /// special keyboard key - public KeyboardControllerPressEventArgs(KeyboardSpecialKey key) - { - SpecialKey = key; - } + var handler = KeyPress; + if (handler != null) + KeyPress(this, new KeyboardControllerPressEventArgs(text)); } /// - /// Enumeration of KeyboardSpecialKey values + /// event helper for special keys /// - public enum KeyboardSpecialKey + /// + void OnKeyPress(KeyboardSpecialKey key) { - /// - /// None - /// - None = 0, - - /// - /// Backspace - /// - Backspace, - - /// - /// Clear - /// - Clear, - - /// - /// GoButton - /// - GoButton, - - /// - /// SecondaryButton - /// - SecondaryButton + var handler = KeyPress; + if (handler != null) + KeyPress(this, new KeyboardControllerPressEventArgs(key)); } + + + /// + /// 2901 + /// + public const uint KeyboardVisible = 2901; + /// + /// 2902 + /// + public const uint ClosePressJoin = 2902; + /// + /// 2903 + /// + public const uint GoButtonPressJoin = 2903; + /// + /// 2903 + /// + public const uint GoButtonTextJoin = 2903; + /// + /// 2904 + /// + public const uint SecondaryButtonPressJoin = 2904; + /// + /// 2904 + /// + public const uint SecondaryButtonTextJoin = 2904; + /// + /// 2905 + /// + public const uint GoButtonVisibleJoin = 2905; + /// + /// 2906 + /// + public const uint SecondaryButtonVisibleJoin = 2906; + /// + /// 2907 + /// + public const uint GoButtonEnableJoin = 2907; + /// + /// 2910 + /// + public const uint ClearPressJoin = 2910; + /// + /// 2911 + /// + public const uint ClearVisibleJoin = 2911; + +} + +/// +/// +/// +public class KeyboardControllerPressEventArgs : EventArgs +{ + public string Text { get; private set; } + public KeyboardSpecialKey SpecialKey { get; private set; } + + public KeyboardControllerPressEventArgs(string text) + { + Text = text; + } + + public KeyboardControllerPressEventArgs(KeyboardSpecialKey key) + { + SpecialKey = key; + } +} + +public enum KeyboardSpecialKey +{ + None = 0, Backspace, Clear, GoButton, SecondaryButton } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Touchpanels/ModalDialog.cs b/src/PepperDash.Essentials.Core/Touchpanels/ModalDialog.cs index e3545cd0..f3bd2e89 100644 --- a/src/PepperDash.Essentials.Core/Touchpanels/ModalDialog.cs +++ b/src/PepperDash.Essentials.Core/Touchpanels/ModalDialog.cs @@ -1,14 +1,10 @@ using System; -using Crestron.SimplSharp; using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core -{ - /// - /// Represents a ModalDialog - /// +namespace PepperDash.Essentials.Core; + public class ModalDialog { /// @@ -19,10 +15,10 @@ namespace PepperDash.Essentials.Core /// Bool press 3992 /// public const uint Button2Join = 3992; - /// - /// 3993 - /// - public const uint CancelButtonJoin = 3993; + /// + /// 3993 + /// + public const uint CancelButtonJoin = 3993; /// ///For visibility of single button. Bool feedback 3994 /// @@ -35,19 +31,19 @@ namespace PepperDash.Essentials.Core /// Shows the timer guage if in use. Bool feedback 3996 /// public const uint TimerVisibleJoin = 3996; - /// - /// Visibility join to show "X" button 3997 - /// - public const uint CancelVisibleJoin = 3997; + /// + /// Visibility join to show "X" button 3997 + /// + public const uint CancelVisibleJoin = 3997; /// /// Shows the modal subpage. Boolean feeback join 3999 /// public const uint ModalVisibleJoin = 3999; - ///// - ///// The seconds value of the countdown timer. Ushort join 3991 - ///// - //public const uint TimerSecondsJoin = 3991; + /// + /// The seconds value of the countdown timer. Ushort join 3991 + /// + //public const uint TimerSecondsJoin = 3991; /// /// The full ushort value of the countdown timer for a gauge. Ushort join 3992 /// @@ -82,10 +78,10 @@ namespace PepperDash.Essentials.Core get { return TriList.BooleanInput[ModalVisibleJoin].BoolValue; } } - /// - /// - /// - public bool CanCancel { get; private set; } + /// + /// + /// + public bool CanCancel { get; private set; } BasicTriList TriList; @@ -103,10 +99,10 @@ namespace PepperDash.Essentials.Core TriList = triList; // Attach actions to buttons - triList.SetSigFalseAction(Button1Join, () => OnModalComplete(1)); + triList.SetSigFalseAction(Button1Join, () => OnModalComplete(1)); triList.SetSigFalseAction(Button2Join, () => OnModalComplete(2)); - triList.SetSigFalseAction(CancelButtonJoin, () => { if (CanCancel) CancelDialog(); }); - CanCancel = true; + triList.SetSigFalseAction(CancelButtonJoin, () => { if (CanCancel) CancelDialog(); }); + CanCancel = true; } /// @@ -156,15 +152,15 @@ namespace PepperDash.Essentials.Core TriList.StringInput[Button2TextJoin].StringValue = button2Text; } // Show/hide guage - TriList.BooleanInput[TimerVisibleJoin].BoolValue = showGauge; - - CanCancel = showCancel; - TriList.BooleanInput[CancelVisibleJoin].BoolValue = showCancel; + TriList.BooleanInput[TimerVisibleJoin].BoolValue = showGauge; + + CanCancel = showCancel; + TriList.BooleanInput[CancelVisibleJoin].BoolValue = showCancel; //Reveal and activate TriList.BooleanInput[ModalVisibleJoin].BoolValue = true; - WakePanel(); + WakePanel(); return true; } @@ -172,49 +168,47 @@ namespace PepperDash.Essentials.Core return false; } - /// - /// WakePanel method - /// - public void WakePanel() - { - try - { - var panel = TriList as TswFt5Button; + /// + /// Wakes the panel by turning on the backlight if off + /// + public void WakePanel() + { + try + { + var panel = TriList as TswFt5Button; - if (panel != null && panel.ExtenderSystemReservedSigs.BacklightOffFeedback.BoolValue) - panel.ExtenderSystemReservedSigs.BacklightOn(); - } - catch - { - Debug.LogMessage(LogEventLevel.Debug, "Error Waking Panel. Maybe testing with Xpanel?"); - } - } + if (panel != null && panel.ExtenderSystemReservedSigs.BacklightOffFeedback.BoolValue) + panel.ExtenderSystemReservedSigs.BacklightOn(); + } + catch + { + Debug.LogMessage(LogEventLevel.Debug, "Error Waking Panel. Maybe testing with Xpanel?"); + } + } - /// - /// CancelDialog method - /// + /// + /// Hide dialog from elsewhere, fires CompleteAction + /// public void CancelDialog() { - OnModalComplete(0); + OnModalComplete(0); } - /// - /// Hides dialog. Fires no action - /// - public void HideDialog() - { - TriList.BooleanInput[ModalVisibleJoin].BoolValue = false; - } + /// + /// Hides dialog. Fires no action + /// + public void HideDialog() + { + TriList.BooleanInput[ModalVisibleJoin].BoolValue = false; + } // When the modal is cleared or times out, clean up the various bits void OnModalComplete(uint buttonNum) { TriList.BooleanInput[ModalVisibleJoin].BoolValue = false; - var action = ModalCompleteAction; - if (action != null) - action(buttonNum); + var action = ModalCompleteAction; + if (action != null) + action(buttonNum); } - } - -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs b/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs index 92764f22..2fb8cb59 100644 --- a/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs +++ b/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs @@ -7,8 +7,8 @@ using PepperDash.Core; using PepperDash.Core.Logging; using Serilog.Events; -namespace PepperDash.Essentials.Core.Touchpanels -{ +namespace PepperDash.Essentials.Core.Touchpanels; + /// /// A wrapper class for the touchpanel portion of an MPC3 class process to allow for configurable /// behavior of the keybad buttons @@ -85,9 +85,9 @@ namespace PepperDash.Essentials.Core.Touchpanels return; } - TryParseInt(key, out int buttonNumber); + TryParseInt(key, out int buttonNumber); - var buttonEventTypes = config.EventTypes; + var buttonEventTypes = config.EventTypes; BoolOutputSig enabledFb = null; BoolOutputSig disabledFb = null; @@ -174,10 +174,10 @@ namespace PepperDash.Essentials.Core.Touchpanels return; } - TryParseInt(key, out int buttonNumber); + TryParseInt(key, out int buttonNumber); - // Link up the button feedbacks to the specified device feedback - var buttonFeedback = config.Feedback; + // Link up the button feedbacks to the specified device feedback + var buttonFeedback = config.Feedback; if (buttonFeedback == null || string.IsNullOrEmpty(buttonFeedback.DeviceKey)) { Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedback not configured, skipping.", @@ -189,14 +189,14 @@ namespace PepperDash.Essentials.Core.Touchpanels try { - if (!(DeviceManager.GetDeviceForKey(buttonFeedback.DeviceKey) is Device device)) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedback deviceKey '{1}' not found.", - key, buttonFeedback.DeviceKey); - return; - } + if (!(DeviceManager.GetDeviceForKey(buttonFeedback.DeviceKey) is Device device)) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedback deviceKey '{1}' not found.", + key, buttonFeedback.DeviceKey); + return; + } - deviceFeedback = device.GetFeedbackProperty(buttonFeedback.FeedbackName); + deviceFeedback = device.GetFeedbackProperty(buttonFeedback.FeedbackName); if (deviceFeedback == null) { Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedbackName property '{1}' not found.", @@ -236,36 +236,36 @@ namespace PepperDash.Essentials.Core.Touchpanels var boolFeedback = deviceFeedback as BoolFeedback; - switch (key) - { - case ("power"): + switch (key) + { + case ("power"): + { + boolFeedback?.LinkCrestronFeedback(_touchpanel.FeedbackPower); + break; + } + case ("volumeup"): + case ("volumedown"): + case ("volumefeedback"): + { + if (deviceFeedback is IntFeedback intFeedback) { - boolFeedback?.LinkCrestronFeedback(_touchpanel.FeedbackPower); - break; + var volumeFeedback = intFeedback; + volumeFeedback.LinkInputSig(_touchpanel.VolumeBargraph); } - case ("volumeup"): - case ("volumedown"): - case ("volumefeedback"): - { - if (deviceFeedback is IntFeedback intFeedback) - { - var volumeFeedback = intFeedback; - volumeFeedback.LinkInputSig(_touchpanel.VolumeBargraph); - } - break; - } - case ("mute"): - { - boolFeedback?.LinkCrestronFeedback(_touchpanel.FeedbackMute); - break; - } - default: - { - boolFeedback?.LinkCrestronFeedback(_touchpanel.Feedbacks[(uint)buttonNumber]); - break; - } - } + break; + } + case ("mute"): + { + boolFeedback?.LinkCrestronFeedback(_touchpanel.FeedbackMute); + break; + } + default: + { + boolFeedback?.LinkCrestronFeedback(_touchpanel.Feedbacks[(uint)buttonNumber]); + break; + } } + } /// /// Try parse int helper method @@ -393,5 +393,4 @@ namespace PepperDash.Essentials.Core.Touchpanels /// [JsonProperty("feedbackName")] public string FeedbackName { get; set; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs b/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs index 2d026dbb..a9713be2 100644 --- a/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs +++ b/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; -using PepperDash.Core; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Extensions used for more-clear attachment of Actions to user objects on sigs /// @@ -18,7 +13,7 @@ namespace PepperDash.Essentials.Core { /// /// Attaches Action to Sig's user object and returns the same Sig. This provides no protection - /// from null sigs + /// from null sigs /// /// The BoolOutputSig to attach the Action to /// An action to run when sig is pressed and when released @@ -74,14 +69,14 @@ namespace PepperDash.Essentials.Core return sig.SetBoolSigAction(b => { if (!b) a(); }); } - /// - /// Sets an action to a held sig - /// - /// The sig - public static BoolOutputSig SetSigHeldAction(this BasicTriList tl, uint sigNum, uint heldMs, Action heldAction) - { - return SetSigHeldAction(tl, sigNum, heldMs, heldAction, null); - } + /// + /// Sets an action to a held sig + /// + /// The sig + public static BoolOutputSig SetSigHeldAction(this BasicTriList tl, uint sigNum, uint heldMs, Action heldAction) + { + return SetSigHeldAction(tl, sigNum, heldMs, heldAction, null); + } /// /// Sets an action to a held sig as well as a released-without-hold action @@ -124,17 +119,14 @@ namespace PepperDash.Essentials.Core } - /// - /// Sets an action to a held sig as well as a released-without-hold action - /// - /// The sig - /// - /// SetSigHeldAction method - /// - public static BoolOutputSig SetSigHeldAction(this BasicTriList tl, uint sigNum, uint heldMs, Action heldAction, Action releaseAction) - { + /// + /// Sets an action to a held sig as well as a released-without-hold action + /// + /// The sig + public static BoolOutputSig SetSigHeldAction(this BasicTriList tl, uint sigNum, uint heldMs, Action heldAction, Action releaseAction) + { return tl.BooleanOutput[sigNum].SetSigHeldAction(heldMs, heldAction, null, releaseAction); - } + } /// /// Sets an action to a held sig, an action for the release of hold, as well as a released-without-hold action @@ -232,35 +224,35 @@ namespace PepperDash.Essentials.Core return ClearSigAction(tl.StringOutput[sigNum]) as StringOutputSig; } - /// - /// ClearAllSigActions method - /// - public static void ClearAllSigActions(this BasicTriList t1) + /// + /// Clears all actions on all sigs + /// + public static void ClearAllSigActions(this BasicTriList t1) + { + foreach (var sig in t1.BooleanOutput) { - foreach (var sig in t1.BooleanOutput) - { - ClearSigAction(sig); - } - - foreach (var sig in t1.UShortOutput) - { - ClearSigAction(sig); - } - - foreach (var sig in t1.StringOutput) - { - ClearSigAction(sig); - } + ClearSigAction(sig); } - /// - /// SetBool method - /// - public static void SetBool(this BasicTriList tl, uint sigNum, bool value) + foreach (var sig in t1.UShortOutput) { - tl.BooleanInput[sigNum].BoolValue = value; + ClearSigAction(sig); } + foreach (var sig in t1.StringOutput) + { + ClearSigAction(sig); + } + } + + /// + /// Helper method to set the value of a bool Sig on TriList + /// + public static void SetBool(this BasicTriList tl, uint sigNum, bool value) + { + tl.BooleanInput[sigNum].BoolValue = value; + } + /// /// Sends an true-false pulse to the sig /// @@ -282,21 +274,21 @@ namespace PepperDash.Essentials.Core tl.BooleanInput[sigNum].Pulse(ms); } - /// - /// Helper method to set the value of a ushort Sig on TriList - /// - public static void SetUshort(this BasicTriList tl, uint sigNum, ushort value) - { - tl.UShortInput[sigNum].UShortValue = value; - } + /// + /// Helper method to set the value of a ushort Sig on TriList + /// + public static void SetUshort(this BasicTriList tl, uint sigNum, ushort value) + { + tl.UShortInput[sigNum].UShortValue = value; + } - /// - /// Helper method to set the value of a string Sig on TriList - /// - public static void SetString(this BasicTriList tl, uint sigNum, string value) - { - tl.StringInput[sigNum].StringValue = value; - } + /// + /// Helper method to set the value of a string Sig on TriList + /// + public static void SetString(this BasicTriList tl, uint sigNum, string value) + { + tl.StringInput[sigNum].StringValue = value; + } /// /// Helper method to set the value of a string Sig on TriList with encoding @@ -343,5 +335,4 @@ namespace PepperDash.Essentials.Core { return tl.StringOutput[sigNum].StringValue; } - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/TriListBridges/HandlerBridge.cs b/src/PepperDash.Essentials.Core/TriListBridges/HandlerBridge.cs index 13676e2d..bd90ede7 100644 --- a/src/PepperDash.Essentials.Core/TriListBridges/HandlerBridge.cs +++ b/src/PepperDash.Essentials.Core/TriListBridges/HandlerBridge.cs @@ -1,32 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro; -using Crestron.SimplSharpPro.DeviceSupport; - - -namespace PepperDash.Essentials.Core + +namespace PepperDash.Essentials.Core; +/// +/// Abstract base class for TriList handler bridges +/// +public abstract class HandlerBridge { /// - /// Abstract base class for TriList handler bridges + /// Gets or sets the IsAttached /// - public abstract class HandlerBridge - { - /// - /// Gets or sets the IsAttached - /// - public bool IsAttached { get; protected set; } + public bool IsAttached { get; protected set; } - /// - /// Attaches the handler to the panel's user objects - /// - public abstract void AttachToTriListOutputs(bool sendUpdate); + /// + /// Attaches the handler to the panel's user objects + /// + public abstract void AttachToTriListOutputs(bool sendUpdate); - /// - /// Removes the handler from the panel's user objects - /// - public abstract void DetachFromTriListOutputs(); - } + /// + /// Removes the handler from the panel's user objects + /// + public abstract void DetachFromTriListOutputs(); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/UI PageManagers/BlurayPageManager.cs b/src/PepperDash.Essentials.Core/UI PageManagers/BlurayPageManager.cs index d14058bd..d5c54a31 100644 --- a/src/PepperDash.Essentials.Core/UI PageManagers/BlurayPageManager.cs +++ b/src/PepperDash.Essentials.Core/UI PageManagers/BlurayPageManager.cs @@ -1,11 +1,7 @@ using Crestron.SimplSharpPro.DeviceSupport; -using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.PageManagers -{ - /// - /// Represents a DiscPlayerMediumPageManager - /// +namespace PepperDash.Essentials.Core.PageManagers; + public class DiscPlayerMediumPageManager : MediumLeftSwitchablePageManager { IDiscPlayerControls Player; @@ -54,5 +50,4 @@ namespace PepperDash.Essentials.Core.PageManagers TriList.BooleanInput[BackingPageJoin].BoolValue = false; TriList.BooleanInput[LeftSubpageJoin].BoolValue = false; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/UI PageManagers/PageManager.cs b/src/PepperDash.Essentials.Core/UI PageManagers/PageManager.cs index ed5f0fff..79d23f19 100644 --- a/src/PepperDash.Essentials.Core/UI PageManagers/PageManager.cs +++ b/src/PepperDash.Essentials.Core/UI PageManagers/PageManager.cs @@ -2,8 +2,8 @@ using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.PageManagers -{ +namespace PepperDash.Essentials.Core.PageManagers; + /// /// The PageManager classes are used to bridge a device to subpage /// visibility. @@ -152,5 +152,4 @@ namespace PepperDash.Essentials.Core.PageManagers { return GetOffsetJoin(DisplayUiType); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/UI PageManagers/SetTopBoxThreePanelPageManager.cs b/src/PepperDash.Essentials.Core/UI PageManagers/SetTopBoxThreePanelPageManager.cs index 338f47dc..ddf598a2 100644 --- a/src/PepperDash.Essentials.Core/UI PageManagers/SetTopBoxThreePanelPageManager.cs +++ b/src/PepperDash.Essentials.Core/UI PageManagers/SetTopBoxThreePanelPageManager.cs @@ -3,226 +3,221 @@ using System.Collections.Generic; using System.Linq; using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Core; -using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Presets; using Serilog.Events; -namespace PepperDash.Essentials.Core.PageManagers +namespace PepperDash.Essentials.Core.PageManagers; + +public class ThreePanelPlusOnePageManager : PageManager { /// - /// Represents a ThreePanelPlusOnePageManager + /// The trilist /// - public class ThreePanelPlusOnePageManager : PageManager - { - /// - /// The trilist - /// - protected BasicTriListWithSmartObject TriList; - - /// - /// Gets or sets the Position5TabsId - /// - public uint Position5TabsId { get; set; } - - /// - /// Show the tabs on the third panel - /// - protected bool ShowPosition5Tabs; - - /// - /// Joins that are always visible when this manager is visible - /// - protected uint[] FixedVisibilityJoins; - - /// - /// Gets or sets the current visible item in position 5 - /// - protected uint CurrentVisiblePosition5Item; - - /// - /// Constructor - /// - /// - public ThreePanelPlusOnePageManager(BasicTriListWithSmartObject trilist) - { - TriList = trilist; - CurrentVisiblePosition5Item = 1; - } - - /// - /// The joins for the switchable panel in position 5 - /// - Dictionary Position5SubpageJoins = new Dictionary - { - { 1, 10053 }, - { 2, 10054 } - }; - - /// - /// - /// - public override void Show() - { - // Project the joins into corresponding sigs. - var fixedSigs = FixedVisibilityJoins.Select(u => TriList.BooleanInput[u]).ToList(); - foreach (var sig in fixedSigs) - sig.BoolValue = true; - - if (ShowPosition5Tabs) - { - // Show selected tab - TriList.BooleanInput[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = true; - // hook up tab object - var tabSo = TriList.SmartObjects[Position5TabsId]; - tabSo.BooleanOutput["Tab Button 1 Press"].UserObject = new Action(b => { if (!b) ShowTab(1); }); - tabSo.BooleanOutput["Tab Button 2 Press"].UserObject = new Action(b => { if (!b) ShowTab(2); }); - tabSo.SigChange -= tabSo_SigChange; - tabSo.SigChange += tabSo_SigChange; - } - } - - void tabSo_SigChange(Crestron.SimplSharpPro.GenericBase currentDevice, Crestron.SimplSharpPro.SmartObjectEventArgs args) - { - var uo = args.Sig.UserObject; - if(uo is Action) - (uo as Action)(args.Sig.BoolValue); - } - - /// - /// Hide method - /// - /// - public override void Hide() - { - var fixedSigs = FixedVisibilityJoins.Select(u => TriList.BooleanInput[u]).ToList(); - foreach (var sig in fixedSigs) - sig.BoolValue = false; - if (ShowPosition5Tabs) - { - TriList.BooleanInput[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = false; - - //var tabSo = TriList.SmartObjects[Position5TabsId]; - //tabSo.BooleanOutput["Tab Button 1 Press"].UserObject = null; - //tabSo.BooleanOutput["Tab Button 2 Press"].UserObject = null; - } - } - - void ShowTab(uint number) - { - // Ignore re-presses - if (CurrentVisiblePosition5Item == number) return; - // Swap subpage - var bi = TriList.BooleanInput; - if (CurrentVisiblePosition5Item > 0) - bi[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = false; - CurrentVisiblePosition5Item = number; - bi[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = true; - } - } - - + protected BasicTriListWithSmartObject TriList; /// - /// Represents a SetTopBoxThreePanelPageManager + /// Gets or sets the Position5TabsId /// - public class SetTopBoxThreePanelPageManager : ThreePanelPlusOnePageManager + public uint Position5TabsId { get; set; } + + /// + /// Show the tabs on the third panel + /// + protected bool ShowPosition5Tabs; + + /// + /// Joins that are always visible when this manager is visible + /// + protected uint[] FixedVisibilityJoins; + + /// + /// Gets or sets the current visible item in position 5 + /// + protected uint CurrentVisiblePosition5Item; + + /// + /// Constructor + /// + /// + public ThreePanelPlusOnePageManager(BasicTriListWithSmartObject trilist) { - ISetTopBoxControls SetTopBox; - DevicePresetsView PresetsView; + TriList = trilist; + CurrentVisiblePosition5Item = 1; + } - /// - /// Gets or sets the DpadSmartObjectId - /// - public uint DpadSmartObjectId { get; set; } - - /// - /// Gets or sets the NumberPadSmartObjectId - /// - public uint NumberPadSmartObjectId { get; set; } - - /// - /// Gets or sets the PresetsSmartObjectId - /// - public uint PresetsSmartObjectId { get; set; } - - /// - /// A page manager for set top box that shows some combination of four different panels, - /// in three slots on the page. - /// - /// - /// - public SetTopBoxThreePanelPageManager(ISetTopBoxControls stb, BasicTriListWithSmartObject trilist) - : base(trilist) - { - SetTopBox = stb; - TriList = trilist; - - DpadSmartObjectId = 10011; - NumberPadSmartObjectId = 10014; - PresetsSmartObjectId = 10012; - Position5TabsId = 10081; - - bool dpad = stb.HasDpad; - bool preset = stb.HasPresets; - bool dvr = stb.HasDvr; - bool numbers = stb.HasNumeric; - - if (dpad && !preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10031, 10091 }; - else if (!dpad && preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10032, 10091 }; - else if (!dpad && !preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10033, 10091 }; - else if (!dpad && !preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10034, 10091 }; - - else if (dpad && preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10042, 10021, 10092 }; - else if (dpad && !preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10043, 10021, 10092 }; - else if (dpad && !preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10021, 10092 }; - else if (!dpad && preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10043, 10022, 10092 }; - else if (!dpad && preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10022, 10092 }; - else if (!dpad && !preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10023, 10092 }; - - else if (dpad && preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10053, 10032, 10011, 10093 }; - else if (dpad && preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10032, 10011, 10093 }; - else if (dpad && !preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10033, 10011, 10093 }; - else if (!dpad && preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10033, 10012, 10093 }; - - else if (dpad && preset && dvr && numbers) - { - FixedVisibilityJoins = new uint[] { 10081, 10032, 10011, 10093 }; // special case - ShowPosition5Tabs = true; - } - // Bad config case - else - { - Debug.LogMessage(LogEventLevel.Debug, stb, "WARNING: Not configured to show any UI elements"); - FixedVisibilityJoins = new uint[] { 10091 }; - } - - // Build presets - if (stb.HasPresets && stb.TvPresets != null) + /// + /// The joins for the switchable panel in position 5 + /// + Dictionary Position5SubpageJoins = new Dictionary { - PresetsView = new DevicePresetsView(trilist, stb.TvPresets); - } - } + { 1, 10053 }, + { 2, 10054 } + }; - /// - /// Show method - /// - /// - public override void Show() - { - if(PresetsView != null) - PresetsView.Attach(); - base.Show(); - } + /// + /// + /// + public override void Show() + { + // Project the joins into corresponding sigs. + var fixedSigs = FixedVisibilityJoins.Select(u => TriList.BooleanInput[u]).ToList(); + foreach (var sig in fixedSigs) + sig.BoolValue = true; - /// - /// Hide method - /// - /// - public override void Hide() + if (ShowPosition5Tabs) { - if (PresetsView != null) - PresetsView.Detach(); - base.Hide(); + // Show selected tab + TriList.BooleanInput[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = true; + // hook up tab object + var tabSo = TriList.SmartObjects[Position5TabsId]; + tabSo.BooleanOutput["Tab Button 1 Press"].UserObject = new Action(b => { if (!b) ShowTab(1); }); + tabSo.BooleanOutput["Tab Button 2 Press"].UserObject = new Action(b => { if (!b) ShowTab(2); }); + tabSo.SigChange -= tabSo_SigChange; + tabSo.SigChange += tabSo_SigChange; } } + + void tabSo_SigChange(Crestron.SimplSharpPro.GenericBase currentDevice, Crestron.SimplSharpPro.SmartObjectEventArgs args) + { + var uo = args.Sig.UserObject; + if (uo is Action) + (uo as Action)(args.Sig.BoolValue); + } + + /// + /// Hide method + /// + /// + public override void Hide() + { + var fixedSigs = FixedVisibilityJoins.Select(u => TriList.BooleanInput[u]).ToList(); + foreach (var sig in fixedSigs) + sig.BoolValue = false; + if (ShowPosition5Tabs) + { + TriList.BooleanInput[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = false; + + //var tabSo = TriList.SmartObjects[Position5TabsId]; + //tabSo.BooleanOutput["Tab Button 1 Press"].UserObject = null; + //tabSo.BooleanOutput["Tab Button 2 Press"].UserObject = null; + } + } + + void ShowTab(uint number) + { + // Ignore re-presses + if (CurrentVisiblePosition5Item == number) return; + // Swap subpage + var bi = TriList.BooleanInput; + if (CurrentVisiblePosition5Item > 0) + bi[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = false; + CurrentVisiblePosition5Item = number; + bi[Position5SubpageJoins[CurrentVisiblePosition5Item]].BoolValue = true; + } +} + + + +/// +/// Represents a SetTopBoxThreePanelPageManager +/// +public class SetTopBoxThreePanelPageManager : ThreePanelPlusOnePageManager +{ + ISetTopBoxControls SetTopBox; + DevicePresetsView PresetsView; + + /// + /// Gets or sets the DpadSmartObjectId + /// + public uint DpadSmartObjectId { get; set; } + + /// + /// Gets or sets the NumberPadSmartObjectId + /// + public uint NumberPadSmartObjectId { get; set; } + + /// + /// Gets or sets the PresetsSmartObjectId + /// + public uint PresetsSmartObjectId { get; set; } + + /// + /// A page manager for set top box that shows some combination of four different panels, + /// in three slots on the page. + /// + /// + /// + public SetTopBoxThreePanelPageManager(ISetTopBoxControls stb, BasicTriListWithSmartObject trilist) + : base(trilist) + { + SetTopBox = stb; + TriList = trilist; + + DpadSmartObjectId = 10011; + NumberPadSmartObjectId = 10014; + PresetsSmartObjectId = 10012; + Position5TabsId = 10081; + + bool dpad = stb.HasDpad; + bool preset = stb.HasPresets; + bool dvr = stb.HasDvr; + bool numbers = stb.HasNumeric; + + if (dpad && !preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10031, 10091 }; + else if (!dpad && preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10032, 10091 }; + else if (!dpad && !preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10033, 10091 }; + else if (!dpad && !preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10034, 10091 }; + + else if (dpad && preset && !dvr && !numbers) FixedVisibilityJoins = new uint[] { 10042, 10021, 10092 }; + else if (dpad && !preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10043, 10021, 10092 }; + else if (dpad && !preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10021, 10092 }; + else if (!dpad && preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10043, 10022, 10092 }; + else if (!dpad && preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10022, 10092 }; + else if (!dpad && !preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10044, 10023, 10092 }; + + else if (dpad && preset && dvr && !numbers) FixedVisibilityJoins = new uint[] { 10053, 10032, 10011, 10093 }; + else if (dpad && preset && !dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10032, 10011, 10093 }; + else if (dpad && !preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10033, 10011, 10093 }; + else if (!dpad && preset && dvr && numbers) FixedVisibilityJoins = new uint[] { 10054, 10033, 10012, 10093 }; + + else if (dpad && preset && dvr && numbers) + { + FixedVisibilityJoins = new uint[] { 10081, 10032, 10011, 10093 }; // special case + ShowPosition5Tabs = true; + } + // Bad config case + else + { + Debug.LogMessage(LogEventLevel.Debug, stb, "WARNING: Not configured to show any UI elements"); + FixedVisibilityJoins = new uint[] { 10091 }; + } + + // Build presets + if (stb.HasPresets && stb.TvPresets != null) + { + PresetsView = new DevicePresetsView(trilist, stb.TvPresets); + } + } + + /// + /// Show method + /// + /// + public override void Show() + { + if (PresetsView != null) + PresetsView.Attach(); + base.Show(); + } + + /// + /// Hide method + /// + /// + public override void Hide() + { + if (PresetsView != null) + PresetsView.Detach(); + base.Hide(); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/UI PageManagers/SetTopBoxTwoPanelPageManager.cs b/src/PepperDash.Essentials.Core/UI PageManagers/SetTopBoxTwoPanelPageManager.cs index 0e5b520a..83482e88 100644 --- a/src/PepperDash.Essentials.Core/UI PageManagers/SetTopBoxTwoPanelPageManager.cs +++ b/src/PepperDash.Essentials.Core/UI PageManagers/SetTopBoxTwoPanelPageManager.cs @@ -2,8 +2,8 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Presets; -namespace PepperDash.Essentials.Core.PageManagers -{ +namespace PepperDash.Essentials.Core.PageManagers; + /// /// Represents a SetTopBoxMediumPageManager /// @@ -68,5 +68,4 @@ namespace PepperDash.Essentials.Core.PageManagers TriList.BooleanInput[BackingPageJoin].BoolValue = false; TriList.BooleanInput[LeftSubpageJoin].BoolValue = false; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/UI PageManagers/SinglePageManager.cs b/src/PepperDash.Essentials.Core/UI PageManagers/SinglePageManager.cs index 3eb0cf80..9afa0555 100644 --- a/src/PepperDash.Essentials.Core/UI PageManagers/SinglePageManager.cs +++ b/src/PepperDash.Essentials.Core/UI PageManagers/SinglePageManager.cs @@ -2,8 +2,8 @@ using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Core.PageManagers -{ +namespace PepperDash.Essentials.Core.PageManagers; + /// /// Represents a SinglePageManager /// @@ -39,5 +39,4 @@ namespace PepperDash.Essentials.Core.PageManagers { TriList.BooleanInput[BackingPageJoin].BoolValue = false; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs b/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs index fd9da802..83601cc4 100644 --- a/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs +++ b/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs @@ -4,177 +4,167 @@ 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 +namespace PepperDash.Essentials.Core.UI; + +public abstract class TouchpanelBase : EssentialsDevice, IHasBasicTriListWithSmartObject { + protected CrestronTouchpanelPropertiesConfig _config; + public BasicTriListWithSmartObject Panel { get; private set; } + /// - /// Base class for Touchpanel devices + /// Constructor for use with device Factory. A touch panel device will be created based on the provided IP-ID and the + /// type of the panel. The SGD File path can be specified using the config property, or a default one located in the program directory if none + /// is provided. /// - public abstract class TouchpanelBase : EssentialsDevice, IHasBasicTriListWithSmartObject + /// Essentials Device Key + /// Essentials Device Name + /// Touchpanel Type to build + /// Touchpanel Configuration + /// IP-ID to use for touch panel + protected TouchpanelBase(string key, string name, BasicTriListWithSmartObject panel, CrestronTouchpanelPropertiesConfig config) + : base(key, name) { - /// - /// Gets or sets the configuration for the Crestron touchpanel. - /// - protected CrestronTouchpanelPropertiesConfig _config; - /// - /// Gets or sets the Panel - /// - public BasicTriListWithSmartObject Panel { get; private set; } - /// - /// Constructor for use with device Factory. A touch panel device will be created based on the provided IP-ID and the - /// type of the panel. The SGD File path can be specified using the config property, or a default one located in the program directory if none - /// is provided. - /// - /// Essentials Device Key - /// Essentials Device Name - /// Crestron Touchpanel Device - /// Touchpanel Configuration - protected TouchpanelBase(string key, string name, BasicTriListWithSmartObject panel, CrestronTouchpanelPropertiesConfig config) - : base(key, name) + if (panel == null) { + Debug.LogMessage(LogEventLevel.Information, this, "Panel is not valid. Touchpanel class WILL NOT work correctly"); + return; + } - if (panel == null) + Panel = panel; + + Panel.SigChange += Panel_SigChange; + + if (Panel is TswFt5ButtonSystem) + { + var tsw = Panel as TswFt5ButtonSystem; + tsw.ExtenderSystemReservedSigs.Use(); + tsw.ExtenderSystemReservedSigs.DeviceExtenderSigChange + += ExtenderSystemReservedSigs_DeviceExtenderSigChange; + + 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); + + // 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, "Panel is not valid. Touchpanel class WILL NOT work correctly"); - return; - } + Debug.LogMessage(LogEventLevel.Information, this, "Smart object file '{0}' not present in User folder. Looking for embedded file", sgdName); - Panel = panel; + sgdName = Global.ApplicationDirectoryPathPrefix + Global.DirectorySeparator + "SGD" + Global.DirectorySeparator + _config.SgdFile; - Panel.SigChange += Panel_SigChange; - - if (Panel is TswFt5ButtonSystem) - { - var tsw = Panel as TswFt5ButtonSystem; - tsw.ExtenderSystemReservedSigs.Use(); - tsw.ExtenderSystemReservedSigs.DeviceExtenderSigChange - += ExtenderSystemReservedSigs_DeviceExtenderSigChange; - - tsw.ButtonStateChange += Tsw_ButtonStateChange; - } - - _config = config; - - AddPreActivationAction(() => - { - // Give up cleanly if SGD is not present. - var sgdName = Global.FilePathPrefix + "sgd" + Global.DirectorySeparator + _config.SgdFile; if (!File.Exists(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)) - { - this.LogWarning("Unable to find SGD file '{0}' in User sgd or application SGD folder. Exiting touchpanel load.", sgdName); - return; - } + Debug.LogMessage(LogEventLevel.Information, this, "Unable to find SGD file '{0}' in User sgd or application SGD folder. Exiting touchpanel load.", sgdName); + return; } + } - Panel.LoadSmartObjects(sgdName); - }); + Panel.LoadSmartObjects(sgdName); + }); - AddPostActivationAction(() => + 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) { - // Check for IEssentialsRoomCombiner in DeviceManager and if found, subscribe to its event + // Subscribe to the even + roomCombiner.RoomCombinationScenarioChanged += new EventHandler(roomCombiner_RoomCombinationScenarioChanged); - if (DeviceManager.AllDevices.FirstOrDefault((d) => d is IEssentialsRoomCombiner) is IEssentialsRoomCombiner roomCombiner) + // Connect to the initial roomKey + if (roomCombiner.CurrentScenario != null) { - // Subscribe to the even - roomCombiner.RoomCombinationScenarioChanged += new EventHandler(RoomCombiner_RoomCombinationScenarioChanged); - - // Connect to the initial roomKey - if (roomCombiner.CurrentScenario != null) - { - // Use the current scenario - DetermineRoomKeyFromScenario(roomCombiner.CurrentScenario); - } - else - { - // Current Scenario not yet set. Use default - SetupPanelDrivers(_config.DefaultRoomKey); - } + // Use the current scenario + DetermineRoomKeyFromScenario(roomCombiner.CurrentScenario); } else { - // No room combiner, use the default key + // Current Scenario not yet set. Use default 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); - }); - } - - /// - /// Setup Panel operation - /// - /// Room Key for this panel - protected abstract void SetupPanelDrivers(string roomKey); - - /// - /// Event handler for System Extender Events - /// - /// - /// - protected abstract void ExtenderSystemReservedSigs_DeviceExtenderSigChange(DeviceExtender currentDeviceExtender, SigEventArgs args); - - - /// - /// - /// - /// - /// - protected virtual void RoomCombiner_RoomCombinationScenarioChanged(object sender, EventArgs e) - { - var roomCombiner = sender as IEssentialsRoomCombiner; - - DetermineRoomKeyFromScenario(roomCombiner.CurrentScenario); - } - - /// - /// Determines the room key to use based on the scenario - /// - /// - protected virtual void DetermineRoomKeyFromScenario(IRoomCombinationScenario scenario) - { - string newRoomKey = null; - - if (scenario.UiMap.ContainsKey(Key)) - { - newRoomKey = scenario.UiMap[Key]; } - else if (scenario.UiMap.ContainsKey(_config.DefaultRoomKey)) + else { - newRoomKey = scenario.UiMap[_config.DefaultRoomKey]; + // No room combiner, use the default key + SetupPanelDrivers(_config.DefaultRoomKey); } + }); + } - SetupPanelDrivers(newRoomKey); - } + /// + /// Setup Panel operation + /// + /// Room Key for this panel + protected abstract void SetupPanelDrivers(string roomKey); - private void Panel_SigChange(object currentDevice, SigEventArgs args) + + /// + /// Event handler for System Extender Events + /// + /// + /// + protected abstract void ExtenderSystemReservedSigs_DeviceExtenderSigChange(DeviceExtender currentDeviceExtender, SigEventArgs args); + + + /// + /// + /// + /// + /// + protected virtual void roomCombiner_RoomCombinationScenarioChanged(object sender, EventArgs e) + { + var roomCombiner = sender as IEssentialsRoomCombiner; + + DetermineRoomKeyFromScenario(roomCombiner.CurrentScenario); + } + + /// + /// Determines the room key to use based on the scenario + /// + /// + protected virtual void DetermineRoomKeyFromScenario(IRoomCombinationScenario scenario) + { + string newRoomKey = null; + + if (scenario.UiMap.ContainsKey(Key)) { - 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); + newRoomKey = scenario.UiMap[Key]; + } + else if (scenario.UiMap.ContainsKey(_config.DefaultRoomKey)) + { + newRoomKey = scenario.UiMap[_config.DefaultRoomKey]; } - 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); - } + 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); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Utilities/ActionSequence.cs b/src/PepperDash.Essentials.Core/Utilities/ActionSequence.cs index 22c9297c..aaea7b66 100644 --- a/src/PepperDash.Essentials.Core/Utilities/ActionSequence.cs +++ b/src/PepperDash.Essentials.Core/Utilities/ActionSequence.cs @@ -2,186 +2,156 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharpPro.CrestronThread; using PepperDash.Core; -using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; using Newtonsoft.Json; using Serilog.Events; -namespace PepperDash.Essentials.Core.Utilities +namespace PepperDash.Essentials.Core.Utilities; + +/// +/// A device that executes a sequence of actions with optional delays between actions +/// +[Description("A device that executes a sequence of actions with optional delays between actions")] +public class ActionSequence : EssentialsDevice { - /// - /// A device that executes a sequence of actions with optional delays between actions - /// - [Description("A device that executes a sequence of actions with optional delays between actions")] - public class ActionSequence : EssentialsDevice + private ActionSequencePropertiesConfig _propertiesConfig; + + private CrestronQueue _actionQueue; + + private Thread _worker; + + private bool _allowActionsToExecute; + + public ActionSequence(string key, DeviceConfig config) + : base(key, config.Name) { - private ActionSequencePropertiesConfig _propertiesConfig; + var props = config.Properties.ToObject(); + _propertiesConfig = props; - private CrestronQueue _actionQueue; - - private Thread _worker; - - private bool _allowActionsToExecute; - - /// - /// Constructor - /// - /// - /// - public ActionSequence(string key, DeviceConfig config) - : base(key, config.Name) + if (_propertiesConfig != null) { - var props = config.Properties.ToObject(); - _propertiesConfig = props; - - if (_propertiesConfig != null) + if (_propertiesConfig.ActionSequence.Count > 0) { - if (_propertiesConfig.ActionSequence.Count > 0) - { - _actionQueue = new CrestronQueue(_propertiesConfig.ActionSequence.Count); - } + _actionQueue = new CrestronQueue(_propertiesConfig.ActionSequence.Count); } } + } - /// - /// StartSequence method - /// - public void StartSequence() + /// + /// Starts executing the sequenced actions + /// + public void StartSequence() + { + if (_worker !=null && _worker.ThreadState == Thread.eThreadStates.ThreadRunning) { - if (_worker !=null && _worker.ThreadState == Thread.eThreadStates.ThreadRunning) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Thread already running. Cannot Start Sequence"); - return; - } - - Debug.LogMessage(LogEventLevel.Debug, this, "Starting Action Sequence"); - _allowActionsToExecute = true; - AddActionsToQueue(); - _worker = new Thread(ProcessActions, null, Thread.eThreadStartOptions.Running); + Debug.LogMessage(LogEventLevel.Debug, this, "Thread already running. Cannot Start Sequence"); + return; } - /// - /// StopSequence method - /// - public void StopSequence() + Debug.LogMessage(LogEventLevel.Debug, this, "Starting Action Sequence"); + _allowActionsToExecute = true; + AddActionsToQueue(); + _worker = new Thread(ProcessActions, null, Thread.eThreadStartOptions.Running); + } + + /// + /// Stops executing the sequenced actions + /// + public void StopSequence() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Action Sequence"); + _allowActionsToExecute = false; + _worker.Abort(); + } + + /// + /// Populates the queue from the configuration information + /// + private void AddActionsToQueue() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Adding {0} actions to queue", _propertiesConfig.ActionSequence.Count); + + for (int i = 0; i < _propertiesConfig.ActionSequence.Count; i++) { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Action Sequence"); - _allowActionsToExecute = false; - _worker.Abort(); + _actionQueue.Enqueue(_propertiesConfig.ActionSequence[i]); } + } - /// - /// Populates the queue from the configuration information - /// - private void AddActionsToQueue() + private object ProcessActions(object obj) + { + while (_allowActionsToExecute && _actionQueue.Count > 0) { - Debug.LogMessage(LogEventLevel.Debug, this, "Adding {0} actions to queue", _propertiesConfig.ActionSequence.Count); + SequencedDeviceActionWrapper action = null; - for (int i = 0; i < _propertiesConfig.ActionSequence.Count; i++) - { - _actionQueue.Enqueue(_propertiesConfig.ActionSequence[i]); - } - } - - private object ProcessActions(object obj) - { - while (_allowActionsToExecute && _actionQueue.Count > 0) - { - SequencedDeviceActionWrapper action = null; - - action = _actionQueue.Dequeue(); - if (action == null) - break; - - // Delay before executing - if (action.DelayMs > 0) - Thread.Sleep(action.DelayMs); - - ExecuteAction(action); - } - - return null; - } - - private void ExecuteAction(DeviceActionWrapper action) - { + action = _actionQueue.Dequeue(); if (action == null) - return; + break; - try - { - DeviceJsonApi.DoDeviceAction(action); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Error Executing Action: {0}", e); - } + // Delay before executing + if (action.DelayMs > 0) + Thread.Sleep(action.DelayMs); + + ExecuteAction(action); } + + return null; } - /// - /// Represents a ActionSequencePropertiesConfig - /// - public class ActionSequencePropertiesConfig + private void ExecuteAction(DeviceActionWrapper action) { - /// - /// Gets or sets the ActionSequence - /// - [JsonProperty("actionSequence")] - public List ActionSequence { get; set; } + if (action == null) + return; - /// - /// Constructor - /// - public ActionSequencePropertiesConfig() + try { - ActionSequence = new List(); + DeviceJsonApi.DoDeviceAction(action); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Error Executing Action: {0}", e); } } +} - /// - /// Represents a SequencedDeviceActionWrapper - /// - public class SequencedDeviceActionWrapper : DeviceActionWrapper +/// +/// Configuration Properties for ActionSequence +/// +public class ActionSequencePropertiesConfig +{ + [JsonProperty("actionSequence")] + public List ActionSequence { get; set; } + + public ActionSequencePropertiesConfig() { - /// - /// Gets or sets the DelayMs - /// - [JsonProperty("delayMs")] - public int DelayMs { get; set; } + ActionSequence = new List(); } +} - /// - /// Represents a ActionSequenceFactory - /// - public class ActionSequenceFactory : EssentialsDeviceFactory +public class SequencedDeviceActionWrapper : DeviceActionWrapper +{ + [JsonProperty("delayMs")] + public int DelayMs { get; set; } +} + +/// +/// Factory class +/// +public class ActionSequenceFactory : EssentialsDeviceFactory +{ + public ActionSequenceFactory() { - /// - /// Constructor - /// - public ActionSequenceFactory() - { - TypeNames = new List() { "actionsequence" }; - } - - /// - /// BuildDevice method - /// - /// device config - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new ActionSequence Device"); - - return new ActionSequence(dc.Key, dc); - } + TypeNames = new List() { "actionsequence" }; } + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new ActionSequence Device"); + + return new ActionSequence(dc.Key, dc); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/VideoStatus/VideoStatusOutputs.cs b/src/PepperDash.Essentials.Core/VideoStatus/VideoStatusOutputs.cs index b5151f79..44fbf31d 100644 --- a/src/PepperDash.Essentials.Core/VideoStatus/VideoStatusOutputs.cs +++ b/src/PepperDash.Essentials.Core/VideoStatus/VideoStatusOutputs.cs @@ -6,8 +6,8 @@ using Crestron.SimplSharp; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DM; -namespace PepperDash.Essentials.Core -{ +namespace PepperDash.Essentials.Core; + /// /// Use this class to pass in values to RoutingInputPorts. Unused properties will have default /// funcs assigned to them. @@ -196,5 +196,4 @@ namespace PepperDash.Essentials.Core // bool VideoSync { get; } //} - //public delegate void VideoStatusChangeHandler(IBasicVideoStatus device, eVideoStatusChangeType type); -} \ No newline at end of file + //public delegate void VideoStatusChangeHandler(IBasicVideoStatus device, eVideoStatusChangeType type); \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index ffbe5509..f73b3f2c 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -8,281 +8,278 @@ using PepperDash.Core.Web; using PepperDash.Essentials.Core.Web.RequestHandlers; using Serilog.Events; -namespace PepperDash.Essentials.Core.Web + +namespace PepperDash.Essentials.Core.Web; + +/// +/// Represents a EssentialsWebApi +/// +public class EssentialsWebApi : EssentialsDevice { - /// - /// Represents a EssentialsWebApi - /// - public class EssentialsWebApi : EssentialsDevice - { - private readonly WebApiServer _server; + private readonly WebApiServer _server; - /// - /// http(s)://{ipaddress}/cws/{basePath} - /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath} - /// - private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance - ? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber) - : "/api"; + /// + /// http(s)://{ipaddress}/cws/{basePath} + /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath} + /// + private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance + ? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber) + : "/api"; - private const int DebugTrace = 0; - private const int DebugInfo = 1; - private const int DebugVerbose = 2; + private const int DebugTrace = 0; + private const int DebugInfo = 1; + private const int DebugVerbose = 2; - /// - /// Gets or sets the BasePath - /// - public string BasePath { get; private set; } + /// + /// Gets or sets the BasePath + /// + public string BasePath { get; private set; } - /// - /// Tracks if CWS is registered - /// - public bool IsRegistered - { - get { return _server.IsRegistered; } - } + /// + /// Tracks if CWS is registered + /// + public bool IsRegistered + { + get { return _server.IsRegistered; } + } - /// - /// Constructor - /// - /// - /// - public EssentialsWebApi(string key, string name) - : this(key, name, null) - { - } + /// + /// Constructor + /// + /// + /// + public EssentialsWebApi(string key, string name) + : this(key, name, null) + { + } - /// - /// Constructor - /// - /// - /// - /// - public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config) - : base(key, name) - { - Key = key; + /// + /// Constructor + /// + /// + /// + /// + public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config) + : base(key, name) + { + Key = key; - if (config == null) - BasePath = _defaultBasePath; - else - BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath; + if (config == null) + BasePath = _defaultBasePath; + else + BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath; - _server = new WebApiServer(Key, Name, BasePath); + _server = new WebApiServer(Key, Name, BasePath); - SetupRoutes(); - } + SetupRoutes(); + } - private void SetupRoutes() - { - var routes = new List - { - new HttpCwsRoute("versions") - { - Name = "ReportVersions", - RouteHandler = new ReportVersionsRequestHandler() - }, - new HttpCwsRoute("appdebug") - { - Name = "AppDebug", - RouteHandler = new AppDebugRequestHandler() - }, - new HttpCwsRoute("devices") - { - Name = "DevList", - RouteHandler = new DevListRequestHandler() - }, - new HttpCwsRoute("deviceCommands/{deviceKey}") - { - Name = "DevJson", - RouteHandler = new DevJsonRequestHandler() - }, - new HttpCwsRoute("deviceProperties/{deviceKey}") - { - Name = "DevProps", - RouteHandler = new DevPropsRequestHandler() - }, - new HttpCwsRoute("deviceMethods/{deviceKey}") - { - Name = "DevMethods", - RouteHandler = new DevMethodsRequestHandler() - }, - new HttpCwsRoute("deviceFeedbacks/{deviceKey}") - { - Name = "GetFeedbacksForDeviceKey", - RouteHandler = new GetFeedbacksForDeviceRequestHandler() - }, - new HttpCwsRoute("deviceStreamDebug") - { - Name = "SetDeviceStreamDebug", - RouteHandler = new SetDeviceStreamDebugRequestHandler() - }, - new HttpCwsRoute("disableAllStreamDebug") - { - Name = "DisableAllStreamDebug", - RouteHandler = new DisableAllStreamDebugRequestHandler() - }, - new HttpCwsRoute("config") - { - Name = "ShowConfig", - RouteHandler = new ShowConfigRequestHandler() - }, - new HttpCwsRoute("types") - { - Name = "GetTypes", - RouteHandler = new GetTypesRequestHandler() - }, - new HttpCwsRoute("types/{filter}") - { - Name = "GetTypesByFilter", - RouteHandler = new GetTypesByFilterRequestHandler() - }, - new HttpCwsRoute("joinMap/{bridgeKey}") - { - Name = "GetJoinMapsForBridgeKey", - RouteHandler = new GetJoinMapForBridgeKeyRequestHandler() - }, - new HttpCwsRoute("joinMap/{bridgeKey}/{deviceKey}") - { - Name = "GetJoinMapsForDeviceKey", - RouteHandler = new GetJoinMapForDeviceKeyRequestHandler() - }, - new HttpCwsRoute("debugSession") - { - Name = "DebugSession", - RouteHandler = new DebugSessionRequestHandler() - }, - new HttpCwsRoute("doNotLoadConfigOnNextBoot") - { - Name = "DoNotLoadConfigOnNextBoot", - RouteHandler = new DoNotLoadConfigOnNextBootRequestHandler() - }, - new HttpCwsRoute("restartProgram") - { - Name = "Restart Program", - RouteHandler = new RestartProgramRequestHandler() - }, - new HttpCwsRoute("loadConfig") - { - Name = "Load Config", - RouteHandler = new LoadConfigRequestHandler() - }, - new HttpCwsRoute("tielines") - { - Name = "Get TieLines", - RouteHandler = new GetTieLinesRequestHandler() - }, - new HttpCwsRoute("device/{deviceKey}/routingPorts") - { - Name = "Get Routing Ports for a device", - RouteHandler = new GetRoutingPortsHandler() - }, - }; - - AddRoute(routes); - } - - /// - /// Add a single route to the API. MUST be done during the activation phase - /// - /// - /// - /// AddRoute method - /// - public void AddRoute(HttpCwsRoute route) + private void SetupRoutes() + { + var routes = new List { - _server.AddRoute(route); - } - - /// - /// Add a collection of routes to the API. MUST be done during the activation phase - /// - /// - public void AddRoute(List routes) - { - foreach (var route in routes) + new HttpCwsRoute("versions") { - AddRoute(route); - } + Name = "ReportVersions", + RouteHandler = new ReportVersionsRequestHandler() + }, + new HttpCwsRoute("appdebug") + { + Name = "AppDebug", + RouteHandler = new AppDebugRequestHandler() + }, + new HttpCwsRoute("devices") + { + Name = "DevList", + RouteHandler = new DevListRequestHandler() + }, + new HttpCwsRoute("deviceCommands/{deviceKey}") + { + Name = "DevJson", + RouteHandler = new DevJsonRequestHandler() + }, + new HttpCwsRoute("deviceProperties/{deviceKey}") + { + Name = "DevProps", + RouteHandler = new DevPropsRequestHandler() + }, + new HttpCwsRoute("deviceMethods/{deviceKey}") + { + Name = "DevMethods", + RouteHandler = new DevMethodsRequestHandler() + }, + new HttpCwsRoute("deviceFeedbacks/{deviceKey}") + { + Name = "GetFeedbacksForDeviceKey", + RouteHandler = new GetFeedbacksForDeviceRequestHandler() + }, + new HttpCwsRoute("deviceStreamDebug") + { + Name = "SetDeviceStreamDebug", + RouteHandler = new SetDeviceStreamDebugRequestHandler() + }, + new HttpCwsRoute("disableAllStreamDebug") + { + Name = "DisableAllStreamDebug", + RouteHandler = new DisableAllStreamDebugRequestHandler() + }, + new HttpCwsRoute("config") + { + Name = "ShowConfig", + RouteHandler = new ShowConfigRequestHandler() + }, + new HttpCwsRoute("types") + { + Name = "GetTypes", + RouteHandler = new GetTypesRequestHandler() + }, + new HttpCwsRoute("types/{filter}") + { + Name = "GetTypesByFilter", + RouteHandler = new GetTypesByFilterRequestHandler() + }, + new HttpCwsRoute("joinMap/{bridgeKey}") + { + Name = "GetJoinMapsForBridgeKey", + RouteHandler = new GetJoinMapForBridgeKeyRequestHandler() + }, + new HttpCwsRoute("joinMap/{bridgeKey}/{deviceKey}") + { + Name = "GetJoinMapsForDeviceKey", + RouteHandler = new GetJoinMapForDeviceKeyRequestHandler() + }, + new HttpCwsRoute("debugSession") + { + Name = "DebugSession", + RouteHandler = new DebugSessionRequestHandler() + }, + new HttpCwsRoute("doNotLoadConfigOnNextBoot") + { + Name = "DoNotLoadConfigOnNextBoot", + RouteHandler = new DoNotLoadConfigOnNextBootRequestHandler() + }, + new HttpCwsRoute("restartProgram") + { + Name = "Restart Program", + RouteHandler = new RestartProgramRequestHandler() + }, + new HttpCwsRoute("loadConfig") + { + Name = "Load Config", + RouteHandler = new LoadConfigRequestHandler() + }, + new HttpCwsRoute("tielines") + { + Name = "Get TieLines", + RouteHandler = new GetTieLinesRequestHandler() + }, + new HttpCwsRoute("device/{deviceKey}/routingPorts") + { + Name = "Get Routing Ports for a device", + RouteHandler = new GetRoutingPortsHandler() + }, + }; + + AddRoute(routes); + } + + /// + /// Add a single route to the API. MUST be done during the activation phase + /// + /// + public void AddRoute(HttpCwsRoute route) + { + _server.AddRoute(route); + } + + /// + /// Add a collection of routes to the API. MUST be done during the activation phase + /// + /// + public void AddRoute(List routes) + { + foreach (var route in routes) + { + AddRoute(route); + } + } + + /// + /// Initializes the CWS class + /// + public override void Initialize() + { + AddRoute(new HttpCwsRoute("apiPaths") + { + Name = "GetPaths", + RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath) + }); + + // If running on an appliance + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) + { + /* + WEBSERVER [ON | OFF | TIMEOUT | MAXSESSIONSPERUSER ] + */ + var response = string.Empty; + CrestronConsole.SendControlSystemCommand("webserver", ref response); + if (response.Contains("OFF")) return; + + var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4); + Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series"); + + _server.Start(); + + GetPaths(); + + return; } - /// - /// Initialize method - /// - /// - public override void Initialize() - { - AddRoute(new HttpCwsRoute("apiPaths") { - Name = "GetPaths", - RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath) - }); + // Automatically start CWS when running on a server (Linux OS, Virtual Control) + Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server"); - // If running on an appliance - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) - { - /* - WEBSERVER [ON | OFF | TIMEOUT | MAXSESSIONSPERUSER ] - */ - var response = string.Empty; - CrestronConsole.SendControlSystemCommand("webserver", ref response); - if (response.Contains("OFF")) return; + _server.Start(); - var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4); - Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series"); + GetPaths(); + } - _server.Start(); + /// + /// Print the available pahts + /// + /// + /// http(s)://{ipaddress}/cws/{basePath} + /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath} + /// + /// + /// GetPaths method + /// + public void GetPaths() + { + Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50)); - GetPaths(); + var currentIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); - return; - } + var hostname = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); - // Automatically start CWS when running on a server (Linux OS, Virtual Control) - Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server"); + var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server + ? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}" + : $"https://{currentIp}/cws{BasePath}"; - _server.Start(); + Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path); - GetPaths(); - } - - /// - /// Print the available pahts - /// - /// - /// http(s)://{ipaddress}/cws/{basePath} - /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath} - /// - /// - /// GetPaths method - /// - public void GetPaths() - { - Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50)); - - var currentIp = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); - - var hostname = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); - - var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server - ? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}" - : $"https://{currentIp}/cws{BasePath}"; - - Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path); - - var routeCollection = _server.GetRouteCollection(); - if (routeCollection == null) - { - Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null"); - return; - } - Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:"); - foreach (var route in routeCollection) - { - Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url); - } - Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50)); - } - } + var routeCollection = _server.GetRouteCollection(); + if (routeCollection == null) + { + Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null"); + return; + } + Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:"); + foreach (var route in routeCollection) + { + Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url); + } + Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50)); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiFactory.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiFactory.cs index 16abd349..4a8f25e0 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiFactory.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiFactory.cs @@ -3,34 +3,33 @@ using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Core.Web +namespace PepperDash.Essentials.Core.Web; + +/// +/// Factory for creating EssentialsWebApi devices +/// +public class EssentialsWebApiFactory : EssentialsDeviceFactory { - /// - /// Represents a EssentialsWebApiFactory - /// - public class EssentialsWebApiFactory : EssentialsDeviceFactory + /// + /// Constructor + /// + public EssentialsWebApiFactory() { - /// - /// Constructor - /// - public EssentialsWebApiFactory() - { - TypeNames = new List { "EssentialsWebApi" }; - } + TypeNames = new List { "EssentialsWebApi" }; + } - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Essentials Web API Server"); + /// + /// BuildDevice method + /// + /// + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Essentials Web API Server"); - var props = dc.Properties.ToObject(); - if (props != null) return new EssentialsWebApi(dc.Key, dc.Name, props); + var props = dc.Properties.ToObject(); + if (props != null) return new EssentialsWebApi(dc.Key, dc.Name, props); - Debug.LogMessage(LogEventLevel.Debug, "Factory failed to create new Essentials Web API Server"); - return null; - } + Debug.LogMessage(LogEventLevel.Debug, "Factory failed to create new Essentials Web API Server"); + return null; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs index 75f90189..243667c0 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs @@ -1,114 +1,112 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using Crestron.SimplSharp.WebScripting; using PepperDash.Core; -namespace PepperDash.Essentials.Core.Web +namespace PepperDash.Essentials.Core.Web; + +/// +/// EssentialsWebApiHelpers class +/// +public static class EssentialsWebApiHelpers { /// - /// EssentialsWebApiHelpers class + /// GetRequestBody method /// - public static class EssentialsWebApiHelpers + public static string GetRequestBody(this HttpCwsRequest request) { - /// - /// GetRequestBody method - /// - public static string GetRequestBody(this HttpCwsRequest request) + var bytes = new byte[request.ContentLength]; + + request.InputStream.Read(bytes, 0, request.ContentLength); + + return Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + + /// + /// MapToAssemblyObject method + /// + public static object MapToAssemblyObject(LoadedAssembly assembly) + { + return new { - var bytes = new byte[request.ContentLength]; + assembly.Name, + assembly.Version + }; + } - request.InputStream.Read(bytes, 0, request.ContentLength); - - return Encoding.UTF8.GetString(bytes, 0, bytes.Length); - } - - /// - /// MapToAssemblyObject method - /// - public static object MapToAssemblyObject(LoadedAssembly assembly) + /// + /// MapToDeviceListObject method + /// + public static object MapToDeviceListObject(IKeyed device) + { + return new { - return new - { - assembly.Name, - assembly.Version - }; - } + device.Key, + Name = (device is IKeyName) + ? (device as IKeyName).Name + : "---" + }; + } - /// - /// MapToDeviceListObject method - /// - public static object MapToDeviceListObject(IKeyed device) + /// + /// MapJoinToObject method + /// + public static object MapJoinToObject(string key, JoinMapBaseAdvanced join) + { + var kp = new KeyValuePair(key, join); + + return MapJoinToObject(kp); + } + + /// + /// MapJoinToObject method + /// + public static object MapJoinToObject(KeyValuePair join) + { + return new { - return new - { - device.Key, - Name = (device is IKeyName) - ? (device as IKeyName).Name - : "---" - }; - } + DeviceKey = join.Key, + Joins = join.Value.Joins.Select(j => MapJoinDataCompleteToObject(j)) + }; + } - /// - /// MapJoinToObject method - /// - public static object MapJoinToObject(string key, JoinMapBaseAdvanced join) + /// + /// MapJoinDataCompleteToObject method + /// + public static object MapJoinDataCompleteToObject(KeyValuePair joinData) + { + return new { - var kp = new KeyValuePair(key, join); + Signal = joinData.Key, + Description = joinData.Value.Metadata.Description, + JoinNumber = joinData.Value.JoinNumber, + JoinSpan = joinData.Value.JoinSpan, + JoinType = joinData.Value.Metadata.JoinType.ToString(), + JoinCapabilities = joinData.Value.Metadata.JoinCapabilities.ToString() + }; + } - return MapJoinToObject(kp); - } + /// + /// MapDeviceTypeToObject method + /// + public static object MapDeviceTypeToObject(string key, DeviceFactoryWrapper device) + { + var kp = new KeyValuePair(key, device); - /// - /// MapJoinToObject method - /// - public static object MapJoinToObject(KeyValuePair join) + return MapDeviceTypeToObject(kp); + } + + /// + /// MapDeviceTypeToObject method + /// + public static object MapDeviceTypeToObject(KeyValuePair device) + { + return new { - return new - { - DeviceKey = join.Key, - Joins = join.Value.Joins.Select(j => MapJoinDataCompleteToObject(j)) - }; - } - - /// - /// MapJoinDataCompleteToObject method - /// - public static object MapJoinDataCompleteToObject(KeyValuePair joinData) - { - return new - { - Signal = joinData.Key, - Description = joinData.Value.Metadata.Description, - JoinNumber = joinData.Value.JoinNumber, - JoinSpan = joinData.Value.JoinSpan, - JoinType = joinData.Value.Metadata.JoinType.ToString(), - JoinCapabilities = joinData.Value.Metadata.JoinCapabilities.ToString() - }; - } - - /// - /// MapDeviceTypeToObject method - /// - public static object MapDeviceTypeToObject(string key, DeviceFactoryWrapper device) - { - var kp = new KeyValuePair(key, device); - - return MapDeviceTypeToObject(kp); - } - - /// - /// MapDeviceTypeToObject method - /// - public static object MapDeviceTypeToObject(KeyValuePair device) - { - return new - { - Type = device.Key, - Description = device.Value.Description, - CType = device.Value.Type == null ? "---": device.Value.Type.ToString() - }; - } + Type = device.Key, + Description = device.Value.Description, + CType = device.Value.Type == null ? "---" : device.Value.Type.ToString() + }; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiPropertiesConfig.cs index 9c00fd93..63e68544 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiPropertiesConfig.cs @@ -1,16 +1,15 @@ using Newtonsoft.Json; -namespace PepperDash.Essentials.Core.Web +namespace PepperDash.Essentials.Core.Web; + +/// +/// Represents a EssentialsWebApiPropertiesConfig +/// +public class EssentialsWebApiPropertiesConfig { /// - /// Represents a EssentialsWebApiPropertiesConfig + /// Gets or sets the BasePath /// - public class EssentialsWebApiPropertiesConfig - { - /// - /// Gets or sets the BasePath - /// - [JsonProperty("basePath")] - public string BasePath { get; set; } - } + [JsonProperty("basePath")] + public string BasePath { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/AppDebugRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/AppDebugRequestHandler.cs index 3d3302cc..2bc8ce09 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/AppDebugRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/AppDebugRequestHandler.cs @@ -2,94 +2,92 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; -using System; using Serilog.Events; using Newtonsoft.Json.Converters; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Represents a AppDebugRequestHandler +/// +public class AppDebugRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a AppDebugRequestHandler - /// - public class AppDebugRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public AppDebugRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public AppDebugRequestHandler() - : base(true) - { - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var appDebug = new AppDebug { MinimumLevel = Debug.WebsocketMinimumLogLevel }; - - var body = JsonConvert.SerializeObject(appDebug, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.Write(body, false); - context.Response.End(); - } - - /// - /// Handles POST method requests - /// - /// - protected override void HandlePost(HttpCwsContext context) - { - if (context.Request.ContentLength < 0) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var data = context.Request.GetRequestBody(); - if (string.IsNullOrEmpty(data)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var appDebug = new AppDebug(); - var requestBody = JsonConvert.DeserializeObject(data); - - Debug.SetWebSocketMinimumDebugLevel(requestBody.MinimumLevel); - - appDebug.MinimumLevel = Debug.WebsocketMinimumLogLevel; - var responseBody = JsonConvert.SerializeObject(appDebug, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.Write(responseBody, false); - context.Response.End(); - } } /// - /// Represents a AppDebug + /// Handles GET method requests /// - public class AppDebug + /// + protected override void HandleGet(HttpCwsContext context) { - /// - /// Gets or sets the MinimumLevel - /// - [JsonProperty("minimumLevel", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(StringEnumConverter))] - public LogEventLevel MinimumLevel { get; set; } + var appDebug = new AppDebug { MinimumLevel = Debug.WebsocketMinimumLogLevel }; + + var body = JsonConvert.SerializeObject(appDebug, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(body, false); + context.Response.End(); } + + /// + /// Handles POST method requests + /// + /// + protected override void HandlePost(HttpCwsContext context) + { + if (context.Request.ContentLength < 0) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var data = context.Request.GetRequestBody(); + if (string.IsNullOrEmpty(data)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var appDebug = new AppDebug(); + var requestBody = JsonConvert.DeserializeObject(data); + + Debug.SetWebSocketMinimumDebugLevel(requestBody.MinimumLevel); + + appDebug.MinimumLevel = Debug.WebsocketMinimumLogLevel; + var responseBody = JsonConvert.SerializeObject(appDebug, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(responseBody, false); + context.Response.End(); + } +} + +/// +/// Represents a AppDebug +/// +public class AppDebug +{ + /// + /// Gets or sets the MinimumLevel + /// + [JsonProperty("minimumLevel", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(StringEnumConverter))] + public LogEventLevel MinimumLevel { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs index 56e2b880..4dbd00a5 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs @@ -1,107 +1,94 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; -using Crestron.SimplSharpPro.EthernetCommunication; using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using Serilog.Events; using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using PepperDash.Essentials.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers -{ +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class DebugSessionRequestHandler : WebApiBaseRequestHandler +{ + private readonly DebugWebsocketSink _sink = new DebugWebsocketSink(); + + public DebugSessionRequestHandler() + : base(true) + { + } + /// - /// Represents a DebugSessionRequestHandler + /// Gets details for a debug session /// - public class DebugSessionRequestHandler : WebApiBaseRequestHandler - { - - private readonly DebugWebsocketSink _sink = new DebugWebsocketSink(); - - /// - /// Constructor - /// - public DebugSessionRequestHandler() - : base(true) + /// + protected override void HandleGet(Crestron.SimplSharp.WebScripting.HttpCwsContext context) + { + var routeData = context.Request.RouteData; + if (routeData == null) { - } - - /// - /// Gets details for a debug session - /// - /// - protected override void HandleGet(Crestron.SimplSharp.WebScripting.HttpCwsContext context) - { - var routeData = context.Request.RouteData; - if (routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - try - { - var ip = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); - - var port = 0; - - if (!_sink.IsRunning) - { - Debug.LogMessage(LogEventLevel.Information, "Starting WS Server"); - // Generate a random port within a specified range - port = new Random().Next(65435, 65535); - // Start the WS Server - _sink.StartServerAndSetPort(port); - Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); - } - - var url = _sink.Url; - - object data = new - { - url = _sink.Url - }; - - Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url); - - // Return the port number with the full url of the WS Server - var res = JsonConvert.SerializeObject(data); - - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = Encoding.UTF8; - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.Write(res, false); - context.Response.End(); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, "Error: {0}", e); - } - } - - /// - /// Stops a debug session - /// - /// - protected override void HandlePost(HttpCwsContext context) - { - _sink.StopServer(); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; context.Response.End(); - Debug.LogMessage(LogEventLevel.Information, "Websocket Debug Session Stopped"); + return; } + try + { + var ip = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + + var port = 0; + + if (!_sink.IsRunning) + { + Debug.LogMessage(LogEventLevel.Information, "Starting WS Server"); + // Generate a random port within a specified range + port = new Random().Next(65435, 65535); + // Start the WS Server + _sink.StartServerAndSetPort(port); + Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); + } + + var url = _sink.Url; + + object data = new + { + url = _sink.Url + }; + + Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url); + + // Return the port number with the full url of the WS Server + var res = JsonConvert.SerializeObject(data); + + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(res, false); + context.Response.End(); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, "Error: {0}", e); + } } + + /// + /// Stops a debug session + /// + /// + protected override void HandlePost(HttpCwsContext context) + { + _sink.StopServer(); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.End(); + + Debug.LogMessage(LogEventLevel.Information, "Websocket Debug Session Stopped"); + } + } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugWebsocketSink.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugWebsocketSink.cs index add30634..34d8b446 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugWebsocketSink.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugWebsocketSink.cs @@ -1,42 +1,41 @@ using System; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class DebugWebsocketSink { - public class DebugWebsocketSink + private bool _isRunning; + private string _url; + + public bool IsRunning => _isRunning; + public string Url => _url; + + public void StartServerAndSetPort(int port) { - private bool _isRunning; - private string _url; - - public bool IsRunning => _isRunning; - public string Url => _url; - - public void StartServerAndSetPort(int port) + try { - try - { - _url = $"ws://localhost:{port}"; - _isRunning = true; - // Implement actual server startup logic here - } - catch (Exception ex) - { - _isRunning = false; - throw new Exception($"Failed to start debug websocket server: {ex.Message}"); - } + _url = $"ws://localhost:{port}"; + _isRunning = true; + // Implement actual server startup logic here } - - public void StopServer() + catch (Exception ex) { - try - { - // Implement actual server shutdown logic here - _isRunning = false; - _url = null; - } - catch (Exception ex) - { - throw new Exception($"Failed to stop debug websocket server: {ex.Message}"); - } + _isRunning = false; + throw new Exception($"Failed to start debug websocket server: {ex.Message}"); + } + } + + public void StopServer() + { + try + { + // Implement actual server shutdown logic here + _isRunning = false; + _url = null; + } + catch (Exception ex) + { + throw new Exception($"Failed to stop debug websocket server: {ex.Message}"); } } } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DefaultRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DefaultRequestHandler.cs index c0fec253..24ad97bc 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DefaultRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DefaultRequestHandler.cs @@ -1,11 +1,8 @@ using Crestron.SimplSharp.WebScripting; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers -{ - /// - /// Represents a DefaultRequestHandler - /// +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + public class DefaultRequestHandler : WebApiBaseRequestHandler { /// @@ -117,5 +114,4 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers context.Response.StatusDescription = "I'm a teapot"; context.Response.End(); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevJsonRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevJsonRequestHandler.cs index 51b261fe..f0cefb1c 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevJsonRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevJsonRequestHandler.cs @@ -5,11 +5,8 @@ using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using Serilog.Events; -namespace PepperDash.Essentials.Core.Web.RequestHandlers -{ - /// - /// Represents a DevJsonRequestHandler - /// +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + public class DevJsonRequestHandler : WebApiBaseRequestHandler { /// @@ -29,25 +26,25 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// protected override void HandlePost(HttpCwsContext context) { - var routeData = context.Request.RouteData; + var routeData = context.Request.RouteData; - if(routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); + if(routeData == null) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); - return; - } + return; + } - if(!routeData.Values.TryGetValue("deviceKey", out var deviceKey)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); + if(!routeData.Values.TryGetValue("deviceKey", out var deviceKey)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); - return; - } + return; + } if (context.Request.ContentLength < 0) { @@ -71,11 +68,11 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers try { - var daw = new DeviceActionWrapper { DeviceKey = (string) deviceKey}; + var daw = new DeviceActionWrapper { DeviceKey = (string) deviceKey}; - JsonConvert.PopulateObject(data, daw); + JsonConvert.PopulateObject(data, daw); - Debug.LogMessage(LogEventLevel.Verbose, "Device Action Wrapper: {@wrapper}", null, daw); + Debug.LogMessage(LogEventLevel.Verbose, "Device Action Wrapper: {@wrapper}", null, daw); DeviceJsonApi.DoDeviceAction(daw); @@ -89,9 +86,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers context.Response.StatusCode = 400; context.Response.StatusDescription = "Bad Request"; - context.Response.Write(JsonConvert.SerializeObject(new { error = ex.Message }), false); + context.Response.Write(JsonConvert.SerializeObject(new { error = ex.Message }), false); context.Response.End(); } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevListRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevListRequestHandler.cs index 31bfd6f9..420cd26c 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevListRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevListRequestHandler.cs @@ -3,52 +3,48 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class DevListRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a DevListRequestHandler - /// - public class DevListRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public DevListRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public DevListRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var allDevices = DeviceManager.AllDevices; + if (allDevices == null) { - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var allDevices = DeviceManager.AllDevices; - if (allDevices == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - allDevices.Sort((a, b) => string.Compare(a.Key, b.Key, System.StringComparison.Ordinal)); - - var deviceList = allDevices.Select(d => EssentialsWebApiHelpers.MapToDeviceListObject(d)).ToList(); - - var js = JsonConvert.SerializeObject(deviceList, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(js, false); + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; context.Response.End(); + + return; } + + allDevices.Sort((a, b) => string.Compare(a.Key, b.Key, System.StringComparison.Ordinal)); + + var deviceList = allDevices.Select(d => EssentialsWebApiHelpers.MapToDeviceListObject(d)).ToList(); + + var js = JsonConvert.SerializeObject(deviceList, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevMethodsRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevMethodsRequestHandler.cs index 004ce5ed..f3bb5c1f 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevMethodsRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevMethodsRequestHandler.cs @@ -1,81 +1,76 @@ using System.Text; using Crestron.SimplSharp.WebScripting; -using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class DevMethodsRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a DevMethodsRequestHandler - /// - public class DevMethodsRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public DevMethodsRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public DevMethodsRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var routeData = context.Request.RouteData; + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Getting DevMethods: {@routeData}", routeData); + if (routeData == null) { - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var routeData = context.Request.RouteData; - Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Getting DevMethods: {@routeData}", routeData); - if (routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - object deviceObj; - if (!routeData.Values.TryGetValue("deviceKey", out deviceObj)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()); - - if (device == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Device Not Found"; - context.Response.End(); - - return; - } - - var deviceMethods = DeviceJsonApi.GetMethods(device.Key); - if (deviceMethods == null || deviceMethods.ToLower().Contains("no device")) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = Encoding.UTF8; - context.Response.Write(deviceMethods, false); + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; context.Response.End(); + + return; } + + object deviceObj; + if (!routeData.Values.TryGetValue("deviceKey", out deviceObj)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()); + + if (device == null) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Device Not Found"; + context.Response.End(); + + return; + } + + var deviceMethods = DeviceJsonApi.GetMethods(device.Key); + if (deviceMethods == null || deviceMethods.ToLower().Contains("no device")) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); + + return; + } + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(deviceMethods, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevPropsRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevPropsRequestHandler.cs index 2117619b..1d51b204 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevPropsRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevPropsRequestHandler.cs @@ -1,79 +1,74 @@ using System.Text; using Crestron.SimplSharp.WebScripting; -using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class DevPropsRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a DevPropsRequestHandler - /// - public class DevPropsRequestHandler : WebApiBaseRequestHandler - { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public DevPropsRequestHandler() - : base(true) - { - } + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public DevPropsRequestHandler() + : base(true) + { + } - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var routeData = context.Request.RouteData; + if (routeData == null) { - var routeData = context.Request.RouteData; - if (routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - object deviceObj; - if (!routeData.Values.TryGetValue("deviceKey", out deviceObj)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()); - - if (device == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Device Not Found"; - context.Response.End(); - - return; - } - - var deviceProperties = DeviceJsonApi.GetProperties(device.Key); - if (deviceProperties == null || deviceProperties.ToLower().Contains("no device")) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = Encoding.UTF8; - context.Response.Write(deviceProperties, false); + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; context.Response.End(); + + return; } + + object deviceObj; + if (!routeData.Values.TryGetValue("deviceKey", out deviceObj)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()); + + if (device == null) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Device Not Found"; + context.Response.End(); + + return; + } + + var deviceProperties = DeviceJsonApi.GetProperties(device.Key); + if (deviceProperties == null || deviceProperties.ToLower().Contains("no device")) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); + + return; + } + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(deviceProperties, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DisableAllStreamDebugRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DisableAllStreamDebugRequestHandler.cs index 172b8c4d..66c5c3a8 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DisableAllStreamDebugRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DisableAllStreamDebugRequestHandler.cs @@ -1,35 +1,31 @@ using Crestron.SimplSharp.WebScripting; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class DisableAllStreamDebugRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a DisableAllStreamDebugRequestHandler - /// - public class DisableAllStreamDebugRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public DisableAllStreamDebugRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public DisableAllStreamDebugRequestHandler() - : base(true) - { - } + } - /// - /// Handles POST method requests - /// - /// - protected override void HandlePost(HttpCwsContext context) - { - DeviceManager.DisableAllDeviceStreamDebugging(); + /// + /// Handles POST method requests + /// + /// + protected override void HandlePost(HttpCwsContext context) + { + DeviceManager.DisableAllDeviceStreamDebugging(); - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.End(); - } + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DoNotLoadConfigOnNextBootRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DoNotLoadConfigOnNextBootRequestHandler.cs index 593ce4e7..b52b0fd2 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DoNotLoadConfigOnNextBootRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DoNotLoadConfigOnNextBootRequestHandler.cs @@ -3,91 +3,87 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class DoNotLoadConfigOnNextBootRequestHandler : WebApiBaseRequestHandler { /// - /// Represents a DoNotLoadConfigOnNextBootRequestHandler + /// Constructor /// - public class DoNotLoadConfigOnNextBootRequestHandler : WebApiBaseRequestHandler + /// + /// base(true) enables CORS support by default + /// + public DoNotLoadConfigOnNextBootRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public DoNotLoadConfigOnNextBootRequestHandler() - : base(true) - { - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var data = new Data - { - DoNotLoadConfigOnNextBoot = Debug.DoNotLoadConfigOnNextBoot - }; - - var body = JsonConvert.SerializeObject(data, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.Write(body, false); - context.Response.End(); - } - - /// - /// Handles POST method requests - /// - /// - protected override void HandlePost(HttpCwsContext context) - { - if (context.Request.ContentLength < 0) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var data = context.Request.GetRequestBody(); - if (string.IsNullOrEmpty(data)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var d = new Data(); - var requestBody = JsonConvert.DeserializeAnonymousType(data, d); - - Debug.SetDoNotLoadConfigOnNextBoot(requestBody.DoNotLoadConfigOnNextBoot); - - var responseBody = JsonConvert.SerializeObject(d, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.Write(responseBody, false); - context.Response.End(); - } } /// - /// Represents a Data + /// Handles GET method requests /// - public class Data + /// + protected override void HandleGet(HttpCwsContext context) { - /// - /// Gets or sets the DoNotLoadConfigOnNextBoot - /// - [JsonProperty("doNotLoadConfigOnNextBoot", NullValueHandling = NullValueHandling.Ignore)] - public bool DoNotLoadConfigOnNextBoot { get; set; } + var data = new Data + { + DoNotLoadConfigOnNextBoot = Debug.DoNotLoadConfigOnNextBoot + }; + + var body = JsonConvert.SerializeObject(data, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(body, false); + context.Response.End(); } + + /// + /// Handles POST method requests + /// + /// + protected override void HandlePost(HttpCwsContext context) + { + if (context.Request.ContentLength < 0) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var data = context.Request.GetRequestBody(); + if (string.IsNullOrEmpty(data)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var d = new Data(); + var requestBody = JsonConvert.DeserializeAnonymousType(data, d); + + Debug.SetDoNotLoadConfigOnNextBoot(requestBody.DoNotLoadConfigOnNextBoot); + + var responseBody = JsonConvert.SerializeObject(d, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(responseBody, false); + context.Response.End(); + } +} + +/// +/// Represents a Data +/// +public class Data +{ + /// + /// Gets or sets the DoNotLoadConfigOnNextBoot + /// + [JsonProperty("doNotLoadConfigOnNextBoot", NullValueHandling = NullValueHandling.Ignore)] + public bool DoNotLoadConfigOnNextBoot { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs index ca9eeb81..5e57b58b 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs @@ -3,103 +3,99 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a GetFeedbacksForDeviceRequestHandler - /// - public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public GetFeedbacksForDeviceRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public GetFeedbacksForDeviceRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var routeData = context.Request.RouteData; + if (routeData == null) { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; } - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) + object deviceObj; + if (!routeData.Values.TryGetValue("deviceKey", out deviceObj)) { - var routeData = context.Request.RouteData; - if (routeData == null) + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + + var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()) as IHasFeedback; + if (device == null) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); + + return; + } + + var boolFeedback = + from feedback in device.Feedbacks.OfType() + where !string.IsNullOrEmpty(feedback.Key) + select new { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - object deviceObj; - if (!routeData.Values.TryGetValue("deviceKey", out deviceObj)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - - var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()) as IHasFeedback; - if (device == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - var boolFeedback = - from feedback in device.Feedbacks.OfType() - where !string.IsNullOrEmpty(feedback.Key) - select new - { - FeedbackKey = feedback.Key, - Value = feedback.BoolValue - }; - - var intFeedback = - from feedback in device.Feedbacks.OfType() - where !string.IsNullOrEmpty(feedback.Key) - select new - { - FeedbackKey = feedback.Key, - Value = feedback.IntValue - }; - - var stringFeedback = - from feedback in device.Feedbacks.OfType() - where !string.IsNullOrEmpty(feedback.Key) - select new - { - FeedbackKey = feedback.Key, - Value = feedback.StringValue ?? string.Empty - }; - - var responseObj = new - { - BoolValues = boolFeedback, - IntValues = intFeedback, - SerialValues = stringFeedback + FeedbackKey = feedback.Key, + Value = feedback.BoolValue }; - var js = JsonConvert.SerializeObject(responseObj, Formatting.Indented); + var intFeedback = + from feedback in device.Feedbacks.OfType() + where !string.IsNullOrEmpty(feedback.Key) + select new + { + FeedbackKey = feedback.Key, + Value = feedback.IntValue + }; - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(js, false); - context.Response.End(); - } + var stringFeedback = + from feedback in device.Feedbacks.OfType() + where !string.IsNullOrEmpty(feedback.Key) + select new + { + FeedbackKey = feedback.Key, + Value = feedback.StringValue ?? string.Empty + }; + + var responseObj = new + { + BoolValues = boolFeedback, + IntValues = intFeedback, + SerialValues = stringFeedback + }; + + var js = JsonConvert.SerializeObject(responseObj, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetJoinMapForBridgeKeyRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetJoinMapForBridgeKeyRequestHandler.cs index 436215ca..ee7ec346 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetJoinMapForBridgeKeyRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetJoinMapForBridgeKeyRequestHandler.cs @@ -4,78 +4,74 @@ using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Bridges; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class GetJoinMapForBridgeKeyRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a GetJoinMapForBridgeKeyRequestHandler - /// - public class GetJoinMapForBridgeKeyRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public GetJoinMapForBridgeKeyRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public GetJoinMapForBridgeKeyRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var routeData = context.Request.RouteData; + if (routeData == null) { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; } - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) + object bridgeObj; + if (!routeData.Values.TryGetValue("bridgeKey", out bridgeObj)) { - var routeData = context.Request.RouteData; - if (routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - object bridgeObj; - if (!routeData.Values.TryGetValue("bridgeKey", out bridgeObj)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var bridge = DeviceManager.GetDeviceForKey(bridgeObj.ToString()) as EiscApiAdvanced; - if (bridge == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var joinMap = bridge.JoinMaps.Select(j => EssentialsWebApiHelpers.MapJoinToObject(j)).ToList(); - if (joinMap == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - var js = JsonConvert.SerializeObject(joinMap, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(js, false); + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; context.Response.End(); - } + + return; + } + + var bridge = DeviceManager.GetDeviceForKey(bridgeObj.ToString()) as EiscApiAdvanced; + if (bridge == null) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var joinMap = bridge.JoinMaps.Select(j => EssentialsWebApiHelpers.MapJoinToObject(j)).ToList(); + if (joinMap == null) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); + + return; + } + + var js = JsonConvert.SerializeObject(joinMap, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetJoinMapForDeviceKeyRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetJoinMapForDeviceKeyRequestHandler.cs index 0cb81d38..54996b8c 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetJoinMapForDeviceKeyRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetJoinMapForDeviceKeyRequestHandler.cs @@ -3,96 +3,92 @@ using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Bridges; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class GetJoinMapForDeviceKeyRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a GetJoinMapForDeviceKeyRequestHandler - /// - public class GetJoinMapForDeviceKeyRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public GetJoinMapForDeviceKeyRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public GetJoinMapForDeviceKeyRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var routeData = context.Request.RouteData; + if (routeData == null) { - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var routeData = context.Request.RouteData; - if (routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - object bridgeObj; - if (!routeData.Values.TryGetValue("bridgeKey", out bridgeObj)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - object deviceObj; - if (!routeData.Values.TryGetValue("deviceKey", out deviceObj)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var bridge = DeviceManager.GetDeviceForKey(bridgeObj.ToString()) as EiscApiAdvanced; - if (bridge == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - JoinMapBaseAdvanced deviceJoinMap; - if (!bridge.JoinMaps.TryGetValue(deviceObj.ToString(), out deviceJoinMap)) - { - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); - - return; - } - - var joinMap = EssentialsWebApiHelpers.MapJoinToObject(deviceObj.ToString(), deviceJoinMap); - var js = JsonConvert.SerializeObject(joinMap, Formatting.Indented, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - NullValueHandling = NullValueHandling.Ignore, - MissingMemberHandling = MissingMemberHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, - TypeNameHandling = TypeNameHandling.None - }); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(js, false); + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; context.Response.End(); + + return; } + + object bridgeObj; + if (!routeData.Values.TryGetValue("bridgeKey", out bridgeObj)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + object deviceObj; + if (!routeData.Values.TryGetValue("deviceKey", out deviceObj)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var bridge = DeviceManager.GetDeviceForKey(bridgeObj.ToString()) as EiscApiAdvanced; + if (bridge == null) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); + + return; + } + + JoinMapBaseAdvanced deviceJoinMap; + if (!bridge.JoinMaps.TryGetValue(deviceObj.ToString(), out deviceJoinMap)) + { + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); + + return; + } + + var joinMap = EssentialsWebApiHelpers.MapJoinToObject(deviceObj.ToString(), deviceJoinMap); + var js = JsonConvert.SerializeObject(joinMap, Formatting.Indented, new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + MissingMemberHandling = MissingMemberHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + TypeNameHandling = TypeNameHandling.None + }); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutesHandler.cs index bd264a7a..c86777b3 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutesHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutesHandler.cs @@ -3,71 +3,49 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class GetRoutesHandler:WebApiBaseRequestHandler { - /// - /// Represents a GetRoutesHandler - /// - public class GetRoutesHandler:WebApiBaseRequestHandler - { - private HttpCwsRouteCollection routeCollection; - private string basePath; + private HttpCwsRouteCollection routeCollection; + private string basePath; - /// - /// Constructor - /// - /// - /// - public GetRoutesHandler(HttpCwsRouteCollection routeCollection, string basePath) { - this.routeCollection = routeCollection; - this.basePath = basePath; - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var currentIp = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); - - var hostname = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); - - var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server - ? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{basePath}" - : $"https://{currentIp}/cws{basePath}"; - - var response = JsonConvert.SerializeObject(new RoutesResponseObject() - { - Url = path, - Routes = routeCollection - }); - - context.Response.StatusCode = 200; - context.Response.ContentType = "application/json"; - context.Response.Headers.Add("Content-Type", "application/json"); - context.Response.Write(response, false); - context.Response.End(); - } + public GetRoutesHandler(HttpCwsRouteCollection routeCollection, string basePath) { + this.routeCollection = routeCollection; + this.basePath = basePath; } - /// - /// Represents a RoutesResponseObject - /// - public class RoutesResponseObject + protected override void HandleGet(HttpCwsContext context) { - /// - /// Gets or sets the Url - /// - [JsonProperty("url")] - public string Url { set; get; } + var currentIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); - /// - /// Gets or sets the Routes - /// - [JsonProperty("routes")] - public HttpCwsRouteCollection Routes { get; set; } + var hostname = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); + + var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server + ? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{basePath}" + : $"https://{currentIp}/cws{basePath}"; + + var response = JsonConvert.SerializeObject(new RoutesResponseObject() + { + Url = path, + Routes = routeCollection + }); + + context.Response.StatusCode = 200; + context.Response.ContentType = "application/json"; + context.Response.Headers.Add("Content-Type", "application/json"); + context.Response.Write(response, false); + context.Response.End(); } } + +public class RoutesResponseObject +{ + [JsonProperty("url")] + public string Url { set; get; } + + [JsonProperty("routes")] + public HttpCwsRouteCollection Routes { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingPortsHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingPortsHandler.cs index 8cee7e9e..b0020c75 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingPortsHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingPortsHandler.cs @@ -5,83 +5,66 @@ using System.Collections.Generic; using System.Linq; using System.Text; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class GetRoutingPortsHandler : WebApiBaseRequestHandler { - /// - /// Represents a GetRoutingPortsHandler - /// - public class GetRoutingPortsHandler : WebApiBaseRequestHandler - { - /// - /// Constructor - /// - public GetRoutingPortsHandler() : base(true) { } + public GetRoutingPortsHandler() : base(true) { } - /// - /// Handles the GET request - /// - /// - protected override void HandleGet(HttpCwsContext context) + protected override void HandleGet(HttpCwsContext context) + { + var routeData = context.Request.RouteData; + + if (routeData == null) { - var routeData = context.Request.RouteData; - - if (routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - return; - } - - if(!routeData.Values.TryGetValue("deviceKey", out var deviceKey)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - return; - } - - var device = DeviceManager.GetDeviceForKey(deviceKey.ToString()); - - if (device == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Device Not Found"; - context.Response.End(); - return; - } - - var inputPorts = (device as IRoutingInputs)?.InputPorts; - var outputPorts = (device as IRoutingOutputs)?.OutputPorts; - - var response = JsonConvert.SerializeObject( new ReturnValue - { - InputPorts = inputPorts?.Select(p => p.Key).ToList(), - OutputPorts = outputPorts?.Select(p => p.Key).ToList() - }); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = Encoding.UTF8; - context.Response.Write(response, false); + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; context.Response.End(); - + return; } - } - internal class ReturnValue - { - /// - /// Gets or sets the InputPorts - /// - [JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)] - public List InputPorts { get; set; } + if (!routeData.Values.TryGetValue("deviceKey", out var deviceKey)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + return; + } + + var device = DeviceManager.GetDeviceForKey(deviceKey.ToString()); + + if (device == null) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Device Not Found"; + context.Response.End(); + return; + } + + var inputPorts = (device as IRoutingInputs)?.InputPorts; + var outputPorts = (device as IRoutingOutputs)?.OutputPorts; + + var response = JsonConvert.SerializeObject(new ReturnValue + { + InputPorts = inputPorts?.Select(p => p.Key).ToList(), + OutputPorts = outputPorts?.Select(p => p.Key).ToList() + }); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(response, false); + context.Response.End(); - /// - /// Gets or sets the OutputPorts - /// - [JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)] - public List OutputPorts { get; set; } } } + +internal class ReturnValue +{ + [JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)] + public List InputPorts { get; set; } + + [JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)] + public List OutputPorts { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTieLinesRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTieLinesRequestHandler.cs index f7088cb2..c8e02126 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTieLinesRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTieLinesRequestHandler.cs @@ -1,43 +1,32 @@ -using Crestron.SimplSharp.WebScripting; +using System.Linq; +using System.Text; +using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -using System.Linq; -using System.Text; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +public class GetTieLinesRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a GetTieLinesRequestHandler - /// - public class GetTieLinesRequestHandler : WebApiBaseRequestHandler + public GetTieLinesRequestHandler() : base(true) { } + + protected override void HandleGet(HttpCwsContext context) { - /// - /// Constructor - /// - public GetTieLinesRequestHandler() : base(true) { } - - /// - /// Handles the GET request - /// - /// - protected override void HandleGet(HttpCwsContext context) + var tieLineString = JsonConvert.SerializeObject(TieLineCollection.Default.Select((tl) => new { - var tieLineString = JsonConvert.SerializeObject(TieLineCollection.Default.Select((tl) => new - { - sourceKey = tl.SourcePort.ParentDevice.Key, - sourcePort = tl.SourcePort.Key, - destinationKey = tl.DestinationPort.ParentDevice.Key, - destinationPort = tl.DestinationPort.Key, - type = tl.Type.ToString(), - })); + sourceKey = tl.SourcePort.ParentDevice.Key, + sourcePort = tl.SourcePort.Key, + destinationKey = tl.DestinationPort.ParentDevice.Key, + destinationPort = tl.DestinationPort.Key, + type = tl.Type.ToString(), + })); - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = Encoding.UTF8; - context.Response.Write(tieLineString, false); - context.Response.End(); + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(tieLineString, false); + context.Response.End(); - } } } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTypesByFilterRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTypesByFilterRequestHandler.cs index 2cabeb5e..fff21d17 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTypesByFilterRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTypesByFilterRequestHandler.cs @@ -3,69 +3,68 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Handles requests for types of devices based on a filter. The filter is used to determine which device factory to pull types from. The filter is passed as part of the URL and is accessed via the RouteData property of the HttpCwsContext. +/// +public class GetTypesByFilterRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a GetTypesByFilterRequestHandler - /// - public class GetTypesByFilterRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public GetTypesByFilterRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public GetTypesByFilterRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var routeData = context.Request.RouteData; + if (routeData == null) { - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var routeData = context.Request.RouteData; - if (routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - object filterObj; - if (!routeData.Values.TryGetValue("filter", out filterObj)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var deviceFactory = DeviceFactory.GetDeviceFactoryDictionary(filterObj.ToString()); - if (deviceFactory == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - var deviceTypes = deviceFactory.Select(t => EssentialsWebApiHelpers.MapDeviceTypeToObject(t)).ToList(); - var js = JsonConvert.SerializeObject(deviceTypes, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(js, false); + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; context.Response.End(); + + return; } + + object filterObj; + if (!routeData.Values.TryGetValue("filter", out filterObj)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var deviceFactory = DeviceFactory.GetDeviceFactoryDictionary(filterObj.ToString()); + if (deviceFactory == null) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); + + return; + } + + var deviceTypes = deviceFactory.Select(t => EssentialsWebApiHelpers.MapDeviceTypeToObject(t)).ToList(); + var js = JsonConvert.SerializeObject(deviceTypes, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTypesRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTypesRequestHandler.cs index 6d011d23..e60700a1 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTypesRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetTypesRequestHandler.cs @@ -3,59 +3,58 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Handles requests for types of devices. This handler is used when no filter is needed to determine which device factory to pull types from. If a filter is needed, use the GetTypesByFilterRequestHandler instead. +/// +public class GetTypesRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a GetTypesRequestHandler - /// - public class GetTypesRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public GetTypesRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public GetTypesRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var routeData = context.Request.RouteData; + if (routeData == null) { - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var routeData = context.Request.RouteData; - if (routeData == null) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var deviceFactory = DeviceFactory.GetDeviceFactoryDictionary(null); - if (deviceFactory == null) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - var deviceTypes = deviceFactory.Select(t => EssentialsWebApiHelpers.MapDeviceTypeToObject(t)).ToList(); - var js = JsonConvert.SerializeObject(deviceTypes, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(js, false); + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; context.Response.End(); + + return; } + + var deviceFactory = DeviceFactory.GetDeviceFactoryDictionary(null); + if (deviceFactory == null) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); + + return; + } + + var deviceTypes = deviceFactory.Select(t => EssentialsWebApiHelpers.MapDeviceTypeToObject(t)).ToList(); + var js = JsonConvert.SerializeObject(deviceTypes, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoadConfigRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoadConfigRequestHandler.cs index 7706973e..adc68c66 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoadConfigRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoadConfigRequestHandler.cs @@ -1,42 +1,38 @@ -using Crestron.SimplSharp; -using Crestron.SimplSharp.WebScripting; -using Newtonsoft.Json; -using PepperDash.Core; +using Crestron.SimplSharp.WebScripting; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Handles requests to load the config. This is used when the config is loaded via the web API instead of at startup. This is typically used in conjunction with the SaveConfigRequestHandler to allow for saving and loading of the config via the web API. +/// +public class LoadConfigRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a LoadConfigRequestHandler - /// - public class LoadConfigRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public LoadConfigRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public LoadConfigRequestHandler() - : base(true) - { - } - - /// - /// Handles POST method requests - /// - /// - protected override void HandlePost(HttpCwsContext context) - { - var message = ""; - var cs = Global.ControlSystem as ILoadConfig; - if(cs != null) - cs.GoWithLoad(); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.Write(message, false); - context.Response.End(); - } + } + + /// + /// Handles POST method requests + /// + /// + protected override void HandlePost(HttpCwsContext context) + { + var message = ""; + var cs = Global.ControlSystem as ILoadConfig; + if (cs != null) + cs.GoWithLoad(); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(message, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/ReportVersionsRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/ReportVersionsRequestHandler.cs index ace863ab..00c33044 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/ReportVersionsRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/ReportVersionsRequestHandler.cs @@ -3,50 +3,49 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Handles requests for reporting versions of loaded assemblies. This is used to report the versions of the assemblies that are currently loaded in the system, which can be helpful for debugging and support purposes. +/// +public class ReportVersionsRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a ReportVersionsRequestHandler - /// - public class ReportVersionsRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public ReportVersionsRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public ReportVersionsRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var loadAssemblies = PluginLoader.LoadedAssemblies; + if (loadAssemblies == null) { - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var loadAssemblies = PluginLoader.LoadedAssemblies; - if (loadAssemblies == null) - { - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); - - return; - } - - var assemblies = loadAssemblies.Select(a => EssentialsWebApiHelpers.MapToAssemblyObject(a)).ToList(); - - var js = JsonConvert.SerializeObject(assemblies, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(js, false); + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; context.Response.End(); + + return; } + + var assemblies = loadAssemblies.Select(a => EssentialsWebApiHelpers.MapToAssemblyObject(a)).ToList(); + + var js = JsonConvert.SerializeObject(assemblies, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RestartProgramRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RestartProgramRequestHandler.cs index 9f853e61..a094657b 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RestartProgramRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RestartProgramRequestHandler.cs @@ -1,41 +1,39 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; -using Newtonsoft.Json; -using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Handles requests to restart the program. +/// This is used when the program needs to be restarted via the web API instead of manually restarting the program. +/// +public class RestartProgramRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a RestartProgramRequestHandler - /// - public class RestartProgramRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public RestartProgramRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public RestartProgramRequestHandler() - : base(true) - { - } - - /// - /// Handles POST method requests - /// - /// - protected override void HandlePost(HttpCwsContext context) - { - var message = ""; - if(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) - CrestronConsole.SendControlSystemCommand($"progres -p:{InitialParametersClass.ApplicationNumber}", ref message); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.Write(message, false); - context.Response.End(); - } + } + + /// + /// Handles POST method requests + /// + /// + protected override void HandlePost(HttpCwsContext context) + { + var message = ""; + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) + CrestronConsole.SendControlSystemCommand($"progres -p:{InitialParametersClass.ApplicationNumber}", ref message); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(message, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs index dd4f416f..bb0525d9 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs @@ -4,228 +4,230 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Handles requests for setting stream debugging on devices that support it. +/// The request is expected to be a POST with a JSON body that includes the device key, the desired debug setting, and an optional timeout in minutes. +/// The handler will attempt to find the device by key, cast it to IStreamDebugging, and then set the debugging according to the provided setting and timeout. +/// If any step fails, an appropriate HTTP error response is returned. +/// +public class SetDeviceStreamDebugRequestHandler : WebApiBaseRequestHandler { /// - /// Represents a SetDeviceStreamDebugRequestHandler + /// Handles CONNECT method requests /// - public class SetDeviceStreamDebugRequestHandler : WebApiBaseRequestHandler + /// + protected override void HandleConnect(HttpCwsContext context) { - /// - /// Handles CONNECT method requests - /// - /// - protected override void HandleConnect(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles DELETE method requests - /// - /// - protected override void HandleDelete(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles HEAD method requests - /// - /// - protected override void HandleHead(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles OPTIONS method requests - /// - /// - protected override void HandleOptions(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles PATCH method requests - /// - /// - protected override void HandlePatch(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles POST method requests - /// - /// - protected override void HandlePost(HttpCwsContext context) - { - if (context.Request.ContentLength < 0) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - var data = context.Request.GetRequestBody(); - if (data == null) - { - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); - - return; - } - - var config = new SetDeviceStreamDebugConfig(); - var body = JsonConvert.DeserializeAnonymousType(data, config); - if (body == null) - { - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); - - return; - } - - if (string.IsNullOrEmpty(body.DeviceKey) || string.IsNullOrEmpty(body.Setting)) - { - context.Response.StatusCode = 400; - context.Response.StatusDescription = "Bad Request"; - context.Response.End(); - - return; - } - - if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device)) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); - - return; - } - - eStreamDebuggingSetting debugSetting; - try - { - debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), body.Setting, true); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Exception handling set debug request"); - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); - - return; - } - - try - { - var mins = Convert.ToUInt32(body.Timeout); - if (mins > 0) - { - device.StreamDebugging.SetDebuggingWithSpecificTimeout(debugSetting, mins); - } - else - { - device.StreamDebugging.SetDebuggingWithDefaultTimeout(debugSetting); - } - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.End(); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Exception handling set debug request"); - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); - } - } - - /// - /// Handles PUT method requests - /// - /// - protected override void HandlePut(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } - - /// - /// Handles TRACE method requests - /// - /// - protected override void HandleTrace(HttpCwsContext context) - { - context.Response.StatusCode = 501; - context.Response.StatusDescription = "Not Implemented"; - context.Response.End(); - } + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); } - /// - /// Configuration class for SetDeviceStreamDebugRequestHandler + /// Handles DELETE method requests /// - public class SetDeviceStreamDebugConfig + /// + protected override void HandleDelete(HttpCwsContext context) { - /// - /// Gets or sets the DeviceKey - /// - [JsonProperty("deviceKey", NullValueHandling = NullValueHandling.Include)] - public string DeviceKey { get; set; } + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } - /// - /// Gets or sets the Setting - /// - [JsonProperty("setting", NullValueHandling = NullValueHandling.Include)] - public string Setting { get; set; } + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } - /// - /// Gets or sets the Timeout - /// - [JsonProperty("timeout")] - public int Timeout { get; set; } + /// + /// Handles HEAD method requests + /// + /// + protected override void HandleHead(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } - /// - /// Constructor - /// - public SetDeviceStreamDebugConfig() + /// + /// Handles OPTIONS method requests + /// + /// + protected override void HandleOptions(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles PATCH method requests + /// + /// + protected override void HandlePatch(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles POST method requests + /// + /// + protected override void HandlePost(HttpCwsContext context) + { + if (context.Request.ContentLength < 0) { - DeviceKey = null; - Setting = null; - Timeout = 15; + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var data = context.Request.GetRequestBody(); + if (data == null) + { + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); + + return; + } + + var config = new SetDeviceStreamDebugConfig(); + var body = JsonConvert.DeserializeAnonymousType(data, config); + if (body == null) + { + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); + + return; + } + + if (string.IsNullOrEmpty(body.DeviceKey) || string.IsNullOrEmpty(body.Setting)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device)) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); + + return; + } + + eStreamDebuggingSetting debugSetting; + try + { + debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), body.Setting, true); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Exception handling set debug request"); + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); + + return; + } + + try + { + var mins = Convert.ToUInt32(body.Timeout); + if (mins > 0) + { + device.StreamDebugging.SetDebuggingWithSpecificTimeout(debugSetting, mins); + } + else + { + device.StreamDebugging.SetDebuggingWithDefaultTimeout(debugSetting); + } + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.End(); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Exception handling set debug request"); + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); } } + + /// + /// Handles PUT method requests + /// + /// + protected override void HandlePut(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } + + /// + /// Handles TRACE method requests + /// + /// + protected override void HandleTrace(HttpCwsContext context) + { + context.Response.StatusCode = 501; + context.Response.StatusDescription = "Not Implemented"; + context.Response.End(); + } +} + + +/// +/// Configuration class for SetDeviceStreamDebugRequestHandler +/// +public class SetDeviceStreamDebugConfig +{ + /// + /// Gets or sets the DeviceKey + /// + [JsonProperty("deviceKey", NullValueHandling = NullValueHandling.Include)] + public string DeviceKey { get; set; } + + /// + /// Gets or sets the Setting + /// + [JsonProperty("setting", NullValueHandling = NullValueHandling.Include)] + public string Setting { get; set; } + + /// + /// Gets or sets the Timeout + /// + [JsonProperty("timeout")] + public int Timeout { get; set; } + + /// + /// Constructor + /// + public SetDeviceStreamDebugConfig() + { + DeviceKey = null; + Setting = null; + Timeout = 15; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/ShowConfigRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/ShowConfigRequestHandler.cs index 4964822e..e053ccce 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/ShowConfigRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/ShowConfigRequestHandler.cs @@ -3,38 +3,39 @@ using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Handles requests to show the current config. +/// This is used to display the current config via the web API. +/// The config is returned as a JSON string. +/// +public class ShowConfigRequestHandler : WebApiBaseRequestHandler { - /// - /// Represents a ShowConfigRequestHandler - /// - public class ShowConfigRequestHandler : WebApiBaseRequestHandler + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public ShowConfigRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public ShowConfigRequestHandler() - : base(true) - { - } + } - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) - { - var config = JsonConvert.SerializeObject(ConfigReader.ConfigObject, Formatting.Indented); + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var config = JsonConvert.SerializeObject(ConfigReader.ConfigObject, Formatting.Indented); - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(config, false); - context.Response.End(); - } + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(config, false); + context.Response.End(); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs b/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs index c0f8ac1e..9b8dd2e5 100644 --- a/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs +++ b/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs @@ -6,152 +6,128 @@ using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common +namespace PepperDash.Essentials.Devices.Common; + +/// +/// Represents and audio endpoint +/// +public class GenericAudioOut : EssentialsDevice, IRoutingSink +{ + public RoutingInputPort CurrentInputPort => AnyAudioIn; + + public event SourceInfoChangeHandler CurrentSourceChange; + + public string CurrentSourceInfoKey { get; set; } + public SourceListItem CurrentSourceInfo + { + get + { + return _CurrentSourceInfo; + } + set + { + if (value == _CurrentSourceInfo) return; + + var handler = CurrentSourceChange; + + if (handler != null) + handler(_CurrentSourceInfo, ChangeType.WillChange); + + _CurrentSourceInfo = value; + + if (handler != null) + handler(_CurrentSourceInfo, ChangeType.DidChange); + } + } + SourceListItem _CurrentSourceInfo; + + /// + /// Gets or sets the AnyAudioIn + /// + public RoutingInputPort AnyAudioIn { get; private set; } + + /// + /// Constructor for GenericAudioOut + /// + /// Device key + /// Device name + public GenericAudioOut(string key, string name) + : base(key, name) + { + AnyAudioIn = new RoutingInputPort(RoutingPortNames.AnyAudioIn, eRoutingSignalType.Audio, + eRoutingPortConnectionType.LineAudio, null, this); + } + + #region IRoutingInputs Members + + /// + /// Gets the collection of input ports + /// + public RoutingPortCollection InputPorts + { + get { return new RoutingPortCollection { AnyAudioIn }; } + } + + #endregion +} + + +/// +/// Represents a GenericAudioOutWithVolume +/// +public class GenericAudioOutWithVolume : GenericAudioOut, IHasVolumeDevice { /// - /// Represents and audio endpoint + /// Gets the volume device key /// - public class GenericAudioOut : EssentialsDevice, IRoutingSink - { - /// - /// Gets the current input port - /// - public RoutingInputPort CurrentInputPort => AnyAudioIn; - - /// - /// Event fired when the current source changes - /// - public event SourceInfoChangeHandler CurrentSourceChange; - - /// - /// Gets or sets the current source info key - /// - public string CurrentSourceInfoKey { get; set; } - /// - /// Gets or sets the current source info - /// - public SourceListItem CurrentSourceInfo - { - get - { - return _CurrentSourceInfo; - } - set - { - if (value == _CurrentSourceInfo) return; - - var handler = CurrentSourceChange; - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.WillChange); - - _CurrentSourceInfo = value; - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.DidChange); - } - } - SourceListItem _CurrentSourceInfo; - - /// - /// Gets or sets the AnyAudioIn - /// - public RoutingInputPort AnyAudioIn { get; private set; } - - /// - /// Constructor for GenericAudioOut - /// - /// Device key - /// Device name - public GenericAudioOut(string key, string name) - : base(key, name) - { - AnyAudioIn = new RoutingInputPort(RoutingPortNames.AnyAudioIn, eRoutingSignalType.Audio, - eRoutingPortConnectionType.LineAudio, null, this); - } - - #region IRoutingInputs Members - - /// - /// Gets the collection of input ports - /// - public RoutingPortCollection InputPorts - { - get { return new RoutingPortCollection { AnyAudioIn }; } - } - - #endregion - } - + public string VolumeDeviceKey { get; private set; } + /// + /// Gets the volume zone + /// + public uint VolumeZone { get; private set; } /// - /// Represents a GenericAudioOutWithVolume + /// Gets the volume device /// - public class GenericAudioOutWithVolume : GenericAudioOut, IHasVolumeDevice + public IBasicVolumeControls VolumeDevice { - /// - /// Gets the volume device key - /// - public string VolumeDeviceKey { get; private set; } - /// - /// Gets the volume zone - /// - public uint VolumeZone { get; private set; } - - /// - /// Gets the volume device - /// - public IBasicVolumeControls VolumeDevice + get { - get - { - var dev = DeviceManager.GetDeviceForKey(VolumeDeviceKey); - if (dev is IAudioZones) - return (dev as IAudioZones).Zone[VolumeZone]; - else return dev as IBasicVolumeControls; - } + var dev = DeviceManager.GetDeviceForKey(VolumeDeviceKey); + if (dev is IAudioZones) + return (dev as IAudioZones).Zone[VolumeZone]; + else return dev as IBasicVolumeControls; } - - /// - /// Constructor - adds the name to the attached audio device, if appropriate. - /// - /// - /// - /// - /// - public GenericAudioOutWithVolume(string key, string name, string audioDevice, uint zone) - : base(key, name) - { - VolumeDeviceKey = audioDevice; - VolumeZone = zone; - } - } /// - /// Factory for creating GenericAudioOutWithVolume devices + /// Constructor - adds the name to the attached audio device, if appropriate. /// - public class GenericAudioOutWithVolumeFactory : EssentialsDeviceFactory + /// + /// + /// + /// + public GenericAudioOutWithVolume(string key, string name, string audioDevice, uint zone) + : base(key, name) { - /// - /// Constructor for GenericAudioOutWithVolumeFactory - /// - public GenericAudioOutWithVolumeFactory() - { - TypeNames = new List() { "genericaudiooutwithvolume" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new GenericAudioOutWithVolumeFactory Device"); - var zone = dc.Properties.Value("zone"); - return new GenericAudioOutWithVolume(dc.Key, dc.Name, - dc.Properties.Value("volumeDeviceKey"), zone); - } + VolumeDeviceKey = audioDevice; + VolumeZone = zone; } +} + +public class GenericAudioOutWithVolumeFactory : EssentialsDeviceFactory +{ + public GenericAudioOutWithVolumeFactory() + { + TypeNames = new List() { "genericaudiooutwithvolume" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new GenericAudioOutWithVolumeFactory Device"); + var zone = dc.Properties.Value("zone"); + return new GenericAudioOutWithVolume(dc.Key, dc.Name, + dc.Properties.Value("volumeDeviceKey"), zone); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/AudioCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/AudioCodecBase.cs index 7f760bc3..dc0f98a6 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/AudioCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/AudioCodecBase.cs @@ -4,117 +4,96 @@ using System.Linq; using PepperDash.Essentials.Core; using PepperDash.Essentials.Devices.Common.Codec; -namespace PepperDash.Essentials.Devices.Common.AudioCodec +namespace PepperDash.Essentials.Devices.Common.AudioCodec; + +public abstract class AudioCodecBase : EssentialsDevice, IHasDialer, IUsageTracking, IAudioCodecInfo { + + public event EventHandler CallStatusChange; + + public AudioCodecInfo CodecInfo { get; protected set; } + + #region IUsageTracking Members + /// - /// Abstract base class for audio codec devices + /// This object can be added by outside users of this class to provide usage tracking + /// for various services /// - public abstract class AudioCodecBase : EssentialsDevice, IHasDialer, IUsageTracking, IAudioCodecInfo + public UsageTracking UsageTracker { get; set; } + + #endregion + + /// + /// Returns true when any call is not in state Unknown, Disconnecting, Disconnected + /// + public bool IsInCall { - - /// - /// Event fired when call status changes - /// - public event EventHandler CallStatusChange; - - /// - /// Gets or sets the CodecInfo - /// - public AudioCodecInfo CodecInfo { get; protected set; } - - #region IUsageTracking Members - - /// - /// Gets or sets the UsageTracker - /// - public UsageTracking UsageTracker { get; set; } - - #endregion - - /// - /// Returns true when any call is not in state Unknown, Disconnecting, Disconnected - /// - public bool IsInCall + get { - get - { - bool value; + bool value; - if (ActiveCalls != null) - value = ActiveCalls.Any(c => c.IsActiveCall); - else - value = false; - return value; - } + if (ActiveCalls != null) + value = ActiveCalls.Any(c => c.IsActiveCall); + else + value = false; + return value; } - - // In most cases only a single call can be active - /// - /// Gets or sets the ActiveCalls - /// - public List ActiveCalls { get; set; } - - /// - /// Constructor for AudioCodecBase - /// - /// Device key - /// Device name - public AudioCodecBase(string key, string name) - : base(key, name) - { - ActiveCalls = new List(); - } - - /// - /// Helper method to fire CallStatusChange event with old and new status - /// - protected void SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus newStatus, CodecActiveCallItem call) - { - call.Status = newStatus; - - OnCallStatusChange(call); - - } - - /// - /// Handles call status change events - /// - /// The call item that changed status - protected void OnCallStatusChange(CodecActiveCallItem item) - { - var handler = CallStatusChange; - if (handler != null) - handler(this, new CodecCallStatusItemChangeEventArgs(item)); - - if (UsageTracker != null) - { - if (IsInCall && !UsageTracker.UsageTrackingStarted) - UsageTracker.StartDeviceUsage(); - else if (UsageTracker.UsageTrackingStarted && !IsInCall) - UsageTracker.EndDeviceUsage(); - } - } - - #region IHasDialer Members - - /// - public abstract void Dial(string number); - - /// - public abstract void EndCall(CodecActiveCallItem activeCall); - - /// - public abstract void EndAllCalls(); - - /// - public abstract void AcceptCall(CodecActiveCallItem item); - - /// - public abstract void RejectCall(CodecActiveCallItem item); - - /// - public abstract void SendDtmf(string digit); - - #endregion } + + // In most cases only a single call can be active + public List ActiveCalls { get; set; } + + public AudioCodecBase(string key, string name) + : base(key, name) + { + ActiveCalls = new List(); + } + + /// + /// Helper method to fire CallStatusChange event with old and new status + /// + protected void SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus newStatus, CodecActiveCallItem call) + { + call.Status = newStatus; + + OnCallStatusChange(call); + + } + + /// + /// + /// + /// + /// + /// + protected void OnCallStatusChange(CodecActiveCallItem item) + { + var handler = CallStatusChange; + if (handler != null) + handler(this, new CodecCallStatusItemChangeEventArgs(item)); + + if (UsageTracker != null) + { + if (IsInCall && !UsageTracker.UsageTrackingStarted) + UsageTracker.StartDeviceUsage(); + else if (UsageTracker.UsageTrackingStarted && !IsInCall) + UsageTracker.EndDeviceUsage(); + } + } + + #region IHasDialer Members + + public abstract void Dial(string number); + + public abstract void EndCall(CodecActiveCallItem activeCall); + + public abstract void EndAllCalls(); + + public abstract void AcceptCall(CodecActiveCallItem item); + + public abstract void RejectCall(CodecActiveCallItem item); + + public abstract void SendDtmf(string digit); + + #endregion } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecInfo.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecInfo.cs index 3e6b0418..6d45515e 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecInfo.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecInfo.cs @@ -1,24 +1,18 @@ -namespace PepperDash.Essentials.Devices.Common.AudioCodec -{ - /// - /// Implements a common set of data about a codec - /// - public interface IAudioCodecInfo - { - /// - /// Gets the codec information - /// - AudioCodecInfo CodecInfo { get; } - } - /// - /// Stores general information about a codec - /// - public abstract class AudioCodecInfo - { - /// - /// Gets or sets the phone number - /// - public abstract string PhoneNumber { get; set; } - } +namespace PepperDash.Essentials.Devices.Common.AudioCodec; + +/// +/// Implements a common set of data about a codec +/// +public interface IAudioCodecInfo +{ + AudioCodecInfo CodecInfo { get; } +} + +/// +/// Stores general information about a codec +/// +public abstract class AudioCodecInfo +{ + public abstract string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IHasAudioCodec.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IHasAudioCodec.cs index 946c2053..df62d915 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IHasAudioCodec.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IHasAudioCodec.cs @@ -1,17 +1,14 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.AudioCodec +namespace PepperDash.Essentials.Devices.Common.AudioCodec; + +/// +/// Interface for devices that have an audio codec. +/// +public interface IHasAudioCodec:IHasInCallFeedback { /// - /// For rooms that have audio codec + /// Gets the audio codec device /// - public interface IHasAudioCodec : IHasInCallFeedback - { - /// - /// Gets the audio codec device - /// - AudioCodecBase AudioCodec { get; } - - //List ActiveCalls { get; } - } + AudioCodecBase AudioCodec { get; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs index 48235a45..b32340af 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs @@ -6,169 +6,121 @@ using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Devices.Common.Codec; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.AudioCodec +namespace PepperDash.Essentials.Devices.Common.AudioCodec; + +public class MockAC : AudioCodecBase { - /// - /// Represents a MockAC - /// - public class MockAC : AudioCodecBase + public MockAC(string key, string name, MockAcPropertiesConfig props) + : base(key, name) { - /// - /// Constructor for MockAC - /// - /// Device key - /// Device name - /// MockAC properties configuration - public MockAC(string key, string name, MockAcPropertiesConfig props) - : base(key, name) - { - CodecInfo = new MockAudioCodecInfo(); + CodecInfo = new MockAudioCodecInfo(); - CodecInfo.PhoneNumber = props.PhoneNumber; - } + CodecInfo.PhoneNumber = props.PhoneNumber; + } - /// - /// Dial method - /// - /// - public override void Dial(string number) + public override void Dial(string number) + { + if (!IsInCall) { - if (!IsInCall) + Debug.LogMessage(LogEventLevel.Debug, this, "Dial: {0}", number); + var call = new CodecActiveCallItem() { - Debug.LogMessage(LogEventLevel.Debug, this, "Dial: {0}", number); - var call = new CodecActiveCallItem() - { - Name = "Mock Outgoing Call", - Number = number, - Type = eCodecCallType.Audio, - Status = eCodecCallStatus.Connected, - Direction = eCodecCallDirection.Outgoing, - Id = "mockAudioCall-1" - }; + Name = "Mock Outgoing Call", + Number = number, + Type = eCodecCallType.Audio, + Status = eCodecCallStatus.Connected, + Direction = eCodecCallDirection.Outgoing, + Id = "mockAudioCall-1" + }; - ActiveCalls.Add(call); - - OnCallStatusChange(call); - } - else - { - Debug.LogMessage(LogEventLevel.Debug, this, "Already in call. Cannot dial new call."); - } - } - - /// - /// EndCall method - /// - /// - public override void EndCall(CodecActiveCallItem call) - { - Debug.LogMessage(LogEventLevel.Debug, this, "EndCall"); - ActiveCalls.Remove(call); - SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); - } - - /// - /// EndAllCalls method - /// - /// - public override void EndAllCalls() - { - Debug.LogMessage(LogEventLevel.Debug, this, "EndAllCalls"); - for (int i = ActiveCalls.Count - 1; i >= 0; i--) - { - var call = ActiveCalls[i]; - ActiveCalls.Remove(call); - SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); - } - } - - /// - /// AcceptCall method - /// - /// - public override void AcceptCall(CodecActiveCallItem call) - { - Debug.LogMessage(LogEventLevel.Debug, this, "AcceptCall"); - SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); - } - - /// - /// RejectCall method - /// - public override void RejectCall(CodecActiveCallItem call) - { - Debug.LogMessage(LogEventLevel.Debug, this, "RejectCall"); - ActiveCalls.Remove(call); - SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); - } - - /// - /// SendDtmf method - /// - /// - public override void SendDtmf(string s) - { - Debug.LogMessage(LogEventLevel.Debug, this, "BEEP BOOP SendDTMF: {0}", s); - } - - /// - /// TestIncomingAudioCall method - /// - /// Phone number to call from - public void TestIncomingAudioCall(string number) - { - Debug.LogMessage(LogEventLevel.Debug, this, "TestIncomingAudioCall from {0}", number); - var call = new CodecActiveCallItem() { Name = number, Id = number, Number = number, Type = eCodecCallType.Audio, Direction = eCodecCallDirection.Incoming }; ActiveCalls.Add(call); - SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Ringing, call); - } + OnCallStatusChange(call); + } + else + { + Debug.LogMessage(LogEventLevel.Debug, this, "Already in call. Cannot dial new call."); + } + } + + public override void EndCall(CodecActiveCallItem call) + { + Debug.LogMessage(LogEventLevel.Debug, this, "EndCall"); + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + } + + public override void EndAllCalls() + { + Debug.LogMessage(LogEventLevel.Debug, this, "EndAllCalls"); + for (int i = ActiveCalls.Count - 1; i >= 0; i--) + { + var call = ActiveCalls[i]; + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + } + } + + public override void AcceptCall(CodecActiveCallItem call) + { + Debug.LogMessage(LogEventLevel.Debug, this, "AcceptCall"); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); + } + + public override void RejectCall(CodecActiveCallItem call) + { + Debug.LogMessage(LogEventLevel.Debug, this, "RejectCall"); + ActiveCalls.Remove(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Disconnected, call); + } + + public override void SendDtmf(string s) + { + Debug.LogMessage(LogEventLevel.Debug, this, "BEEP BOOP SendDTMF: {0}", s); } /// - /// Represents a MockAudioCodecInfo + /// /// - public class MockAudioCodecInfo : AudioCodecInfo + /// + public void TestIncomingAudioCall(string number) { - string _phoneNumber; - - /// - public override string PhoneNumber - { - get - { - return _phoneNumber; - } - set - { - _phoneNumber = value; - } - } + Debug.LogMessage(LogEventLevel.Debug, this, "TestIncomingAudioCall from {0}", number); + var call = new CodecActiveCallItem() { Name = number, Id = number, Number = number, Type = eCodecCallType.Audio, Direction = eCodecCallDirection.Incoming }; + ActiveCalls.Add(call); + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Ringing, call); } - /// - /// Represents a MockACFactory - /// - public class MockACFactory : EssentialsDeviceFactory - { - /// - /// Constructor for MockACFactory - /// - public MockACFactory() - { - TypeNames = new List() { "mockac" }; - } +} - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) +public class MockAudioCodecInfo : AudioCodecInfo +{ + string _phoneNumber; + + public override string PhoneNumber + { + get { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MockAc Device"); - var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); - return new AudioCodec.MockAC(dc.Key, dc.Name, props); + return _phoneNumber; + } + set + { + _phoneNumber = value; } } +} +public class MockACFactory : EssentialsDeviceFactory +{ + public MockACFactory() + { + TypeNames = new List() { "mockac" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MockAc Device"); + var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); + return new AudioCodec.MockAC(dc.Key, dc.Name, props); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAcPropertiesConfig.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAcPropertiesConfig.cs index 0deaca97..7ac205bb 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAcPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAcPropertiesConfig.cs @@ -1,16 +1,9 @@ using Newtonsoft.Json; -namespace PepperDash.Essentials.Devices.Common.AudioCodec +namespace PepperDash.Essentials.Devices.Common.AudioCodec; + +public class MockAcPropertiesConfig { - /// - /// Represents a MockAcPropertiesConfig - /// - public class MockAcPropertiesConfig - { - /// - /// Gets or sets the PhoneNumber - /// - [JsonProperty("phoneNumber")] - public string PhoneNumber { get; set; } - } + [JsonProperty("phoneNumber")] + public string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraBase.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraBase.cs index e2e15668..a7b880c9 100644 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraBase.cs @@ -13,337 +13,266 @@ using PepperDash.Essentials.Core.Devices; using PepperDash.Essentials.Core.Presets; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Cameras +namespace PepperDash.Essentials.Devices.Common.Cameras; + +public enum eCameraCapabilities { - /// - /// Enumeration of eCameraCapabilities values - /// - public enum eCameraCapabilities + None = 0, + Pan = 1, + Tilt = 2, + Zoom = 4, + Focus = 8 +} + +public abstract class CameraBase : ReconfigurableDevice, IRoutingOutputs + { + [JsonProperty("controlMode", NullValueHandling = NullValueHandling.Ignore)] + public eCameraControlMode ControlMode { get; protected set; } + + #region IRoutingOutputs Members + + [JsonIgnore] + public RoutingPortCollection OutputPorts { get; protected set; } + + #endregion + + [JsonProperty("canPan", NullValueHandling = NullValueHandling.Ignore)] + public bool CanPan { - /// - /// No camera capabilities - /// - None = 0, - /// - /// Camera supports pan movement - /// - Pan = 1, - /// - /// Camera supports tilt movement - /// - Tilt = 2, - /// - /// Camera supports zoom functionality - /// - Zoom = 4, - /// - /// Camera supports focus adjustment - /// - Focus = 8 + get + { + return (Capabilities & eCameraCapabilities.Pan) == eCameraCapabilities.Pan; + } + } + [JsonProperty("canTilt", NullValueHandling = NullValueHandling.Ignore)] + public bool CanTilt + { + get + { + return (Capabilities & eCameraCapabilities.Tilt) == eCameraCapabilities.Tilt; + } + } + [JsonProperty("canZoom", NullValueHandling = NullValueHandling.Ignore)] + public bool CanZoom + { + get + { + return (Capabilities & eCameraCapabilities.Zoom) == eCameraCapabilities.Zoom; + } + } + [JsonProperty("canFocus", NullValueHandling = NullValueHandling.Ignore)] + public bool CanFocus + { + get + { + return (Capabilities & eCameraCapabilities.Focus) == eCameraCapabilities.Focus; + } } - /// - /// Abstract base class for camera devices that provides common camera functionality and capabilities - /// - public abstract class CameraBase : ReconfigurableDevice, IRoutingOutputs + // A bitmasked value to indicate the movement capabilites of this camera + protected eCameraCapabilities Capabilities { get; set; } + + protected CameraBase(DeviceConfig config) : base(config) + { + OutputPorts = new RoutingPortCollection(); + + ControlMode = eCameraControlMode.Manual; + + } + + protected CameraBase(string key, string name) : + this (new DeviceConfig{Name = name, Key = key}) { - /// - /// Gets or sets the ControlMode - /// - [JsonProperty("controlMode", NullValueHandling = NullValueHandling.Ignore)] - public eCameraControlMode ControlMode { get; protected set; } + + } - #region IRoutingOutputs Members + protected void LinkCameraToApi(CameraBase cameraDevice, BasicTriList trilist, uint joinStart, string joinMapKey, + EiscApiAdvanced bridge) + { + CameraControllerJoinMap joinMap = new CameraControllerJoinMap(joinStart); - /// - /// Gets or sets the OutputPorts - /// - [JsonIgnore] - public RoutingPortCollection OutputPorts { get; protected set; } - - #endregion - - /// - /// Gets a value indicating whether this camera supports pan movement - /// - [JsonProperty("canPan", NullValueHandling = NullValueHandling.Ignore)] - public bool CanPan + if (bridge != null) { - get - { - return (Capabilities & eCameraCapabilities.Pan) == eCameraCapabilities.Pan; - } + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); } - /// - /// Gets a value indicating whether this camera supports tilt movement - /// - [JsonProperty("canTilt", NullValueHandling = NullValueHandling.Ignore)] - public bool CanTilt + var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey); + + if (customJoins != null) { - get - { - return (Capabilities & eCameraCapabilities.Tilt) == eCameraCapabilities.Tilt; - } + joinMap.SetCustomJoinData(customJoins); } - /// - /// Gets a value indicating whether this camera supports zoom functionality - /// - [JsonProperty("canZoom", NullValueHandling = NullValueHandling.Ignore)] - public bool CanZoom + Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); + Debug.LogMessage(LogEventLevel.Information, "Linking to Bridge Type {0}", cameraDevice.GetType().Name.ToString()); + + var commMonitor = cameraDevice as ICommunicationMonitor; + commMonitor.CommunicationMonitor.IsOnlineFeedback.LinkInputSig( + trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); + + var ptzCamera = cameraDevice as IHasCameraPtzControl; + + if (ptzCamera != null) { - get + trilist.SetBoolSigAction(joinMap.PanLeft.JoinNumber, (b) => { - return (Capabilities & eCameraCapabilities.Zoom) == eCameraCapabilities.Zoom; - } - } - - /// - /// Gets a value indicating whether this camera supports focus adjustment - /// - [JsonProperty("canFocus", NullValueHandling = NullValueHandling.Ignore)] - public bool CanFocus - { - get - { - return (Capabilities & eCameraCapabilities.Focus) == eCameraCapabilities.Focus; - } - } - - /// - /// Gets or sets a bitmasked value to indicate the movement capabilities of this camera - /// - protected eCameraCapabilities Capabilities { get; set; } - - /// - /// Initializes a new instance of the CameraBase class with the specified device configuration - /// - /// The device configuration - protected CameraBase(DeviceConfig config) : base(config) - { - OutputPorts = new RoutingPortCollection(); - - ControlMode = eCameraControlMode.Manual; - - } - - /// - /// Initializes a new instance of the CameraBase class with the specified key and name - /// - /// The unique key for this camera device - /// The friendly name for this camera device - protected CameraBase(string key, string name) : - this(new DeviceConfig { Name = name, Key = key }) - { - - } - - /// - /// Links the camera device to the API bridge for control and feedback - /// - /// The camera device to link - /// The trilist for communication - /// The starting join number for the camera controls - /// The join map key for custom join mappings - /// The EiscApiAdvanced bridge for advanced join mapping - protected void LinkCameraToApi(CameraBase cameraDevice, BasicTriList trilist, uint joinStart, string joinMapKey, - EiscApiAdvanced bridge) - { - CameraControllerJoinMap joinMap = new CameraControllerJoinMap(joinStart); - - if (bridge != null) - { - bridge.AddJoinMap(Key, joinMap); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); - } - - var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey); - - if (customJoins != null) - { - joinMap.SetCustomJoinData(customJoins); - } - - Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); - Debug.LogMessage(LogEventLevel.Information, "Linking to Bridge Type {0}", cameraDevice.GetType().Name.ToString()); - - var commMonitor = cameraDevice as ICommunicationMonitor; - commMonitor.CommunicationMonitor.IsOnlineFeedback.LinkInputSig( - trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); - - var ptzCamera = cameraDevice as IHasCameraPtzControl; - - if (ptzCamera != null) - { - trilist.SetBoolSigAction(joinMap.PanLeft.JoinNumber, (b) => + if (b) { - if (b) - { - ptzCamera.PanLeft(); - } - else - { - ptzCamera.PanStop(); - } - }); - trilist.SetBoolSigAction(joinMap.PanRight.JoinNumber, (b) => - { - if (b) - { - ptzCamera.PanRight(); - } - else - { - ptzCamera.PanStop(); - } - }); - - trilist.SetBoolSigAction(joinMap.TiltUp.JoinNumber, (b) => - { - if (b) - { - ptzCamera.TiltUp(); - } - else - { - ptzCamera.TiltStop(); - } - }); - trilist.SetBoolSigAction(joinMap.TiltDown.JoinNumber, (b) => - { - if (b) - { - ptzCamera.TiltDown(); - } - else - { - ptzCamera.TiltStop(); - } - }); - - trilist.SetBoolSigAction(joinMap.ZoomIn.JoinNumber, (b) => - { - if (b) - { - ptzCamera.ZoomIn(); - } - else - { - ptzCamera.ZoomStop(); - } - }); - - trilist.SetBoolSigAction(joinMap.ZoomOut.JoinNumber, (b) => - { - if (b) - { - ptzCamera.ZoomOut(); - } - else - { - ptzCamera.ZoomStop(); - } - }); - } - - var powerCamera = cameraDevice as IHasPowerControl; - if (powerCamera != null) - { - trilist.SetSigTrueAction(joinMap.PowerOn.JoinNumber, () => powerCamera.PowerOn()); - trilist.SetSigTrueAction(joinMap.PowerOff.JoinNumber, () => powerCamera.PowerOff()); - - var powerFbCamera = powerCamera as IHasPowerControlWithFeedback; - if (powerFbCamera != null) - { - powerFbCamera.PowerIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PowerOn.JoinNumber]); - powerFbCamera.PowerIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.PowerOff.JoinNumber]); + ptzCamera.PanLeft(); } - } - - if (cameraDevice is ICommunicationMonitor) - { - var monitoredCamera = cameraDevice as ICommunicationMonitor; - monitoredCamera.CommunicationMonitor.IsOnlineFeedback.LinkInputSig( - trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); - } - - if (cameraDevice is IHasCameraPresets) - { - // Set the preset lables when they change - var presetsCamera = cameraDevice as IHasCameraPresets; - presetsCamera.PresetsListHasChanged += new EventHandler((o, a) => + else { - SendCameraPresetNamesToApi(presetsCamera, joinMap, trilist); + ptzCamera.PanStop(); + } + }); + trilist.SetBoolSigAction(joinMap.PanRight.JoinNumber, (b) => + { + if (b) + { + ptzCamera.PanRight(); + } + else + { + ptzCamera.PanStop(); + } + }); + + trilist.SetBoolSigAction(joinMap.TiltUp.JoinNumber, (b) => + { + if (b) + { + ptzCamera.TiltUp(); + } + else + { + ptzCamera.TiltStop(); + } + }); + trilist.SetBoolSigAction(joinMap.TiltDown.JoinNumber, (b) => + { + if (b) + { + ptzCamera.TiltDown(); + } + else + { + ptzCamera.TiltStop(); + } + }); + + trilist.SetBoolSigAction(joinMap.ZoomIn.JoinNumber, (b) => + { + if (b) + { + ptzCamera.ZoomIn(); + } + else + { + ptzCamera.ZoomStop(); + } + }); + + trilist.SetBoolSigAction(joinMap.ZoomOut.JoinNumber, (b) => + { + if (b) + { + ptzCamera.ZoomOut(); + } + else + { + ptzCamera.ZoomStop(); + } + }); + } + + var powerCamera = cameraDevice as IHasPowerControl; + if (powerCamera != null) + { + trilist.SetSigTrueAction(joinMap.PowerOn.JoinNumber, () => powerCamera.PowerOn()); + trilist.SetSigTrueAction(joinMap.PowerOff.JoinNumber, () => powerCamera.PowerOff()); + + var powerFbCamera = powerCamera as IHasPowerControlWithFeedback; + if (powerFbCamera != null) + { + powerFbCamera.PowerIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PowerOn.JoinNumber]); + powerFbCamera.PowerIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.PowerOff.JoinNumber]); + } + } + + if (cameraDevice is ICommunicationMonitor) + { + var monitoredCamera = cameraDevice as ICommunicationMonitor; + monitoredCamera.CommunicationMonitor.IsOnlineFeedback.LinkInputSig( + trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); + } + + if (cameraDevice is IHasCameraPresets) + { + // Set the preset lables when they change + var presetsCamera = cameraDevice as IHasCameraPresets; + presetsCamera.PresetsListHasChanged += new EventHandler((o, a) => + { + SendCameraPresetNamesToApi(presetsCamera, joinMap, trilist); + }); + + SendCameraPresetNamesToApi(presetsCamera, joinMap, trilist); + + for (int i = 0; i < joinMap.PresetRecallStart.JoinSpan; i++) + { + int tempNum = i; + + trilist.SetSigTrueAction((ushort) (joinMap.PresetRecallStart.JoinNumber + tempNum), () => + { + presetsCamera.PresetSelect(tempNum); }); + trilist.SetSigTrueAction((ushort) (joinMap.PresetSaveStart.JoinNumber + tempNum), () => + { + var label = trilist.GetString((ushort) (joinMap.PresetLabelStart.JoinNumber + tempNum)); + + presetsCamera.PresetStore(tempNum, label); + }); + } + trilist.OnlineStatusChange += (sender, args) => + { + if (!args.DeviceOnLine) + { return; } SendCameraPresetNamesToApi(presetsCamera, joinMap, trilist); + }; - for (int i = 0; i < joinMap.PresetRecallStart.JoinSpan; i++) - { - int tempNum = i; - - trilist.SetSigTrueAction((ushort)(joinMap.PresetRecallStart.JoinNumber + tempNum), () => - { - presetsCamera.PresetSelect(tempNum); - }); - trilist.SetSigTrueAction((ushort)(joinMap.PresetSaveStart.JoinNumber + tempNum), () => - { - var label = trilist.GetString((ushort)(joinMap.PresetLabelStart.JoinNumber + tempNum)); - - presetsCamera.PresetStore(tempNum, label); - }); - } - trilist.OnlineStatusChange += (sender, args) => - { - if (!args.DeviceOnLine) - { return; } - - SendCameraPresetNamesToApi(presetsCamera, joinMap, trilist); - }; - - } - } - private void SendCameraPresetNamesToApi(IHasCameraPresets presetsCamera, CameraControllerJoinMap joinMap, BasicTriList trilist) - { - for (int i = 1; i <= joinMap.NumberOfPresets.JoinNumber; i++) - { - int tempNum = i - 1; - - string label = ""; - - var preset = presetsCamera.Presets.FirstOrDefault(p => p.ID.Equals(i)); - - if (preset != null) - label = preset.Description; - - trilist.SetString((ushort)(joinMap.PresetLabelStart.JoinNumber + tempNum), label); - } } } - - - /// - /// Represents a CameraPreset - /// - public class CameraPreset : PresetBase + private void SendCameraPresetNamesToApi(IHasCameraPresets presetsCamera, CameraControllerJoinMap joinMap, BasicTriList trilist) { - /// - /// Initializes a new instance of the CameraPreset class - /// - /// The preset ID - /// The preset description - /// Whether the preset is defined - /// Whether the preset can be defined - public CameraPreset(int id, string description, bool isDefined, bool isDefinable) - : base(id, description, isDefined, isDefinable) + for (int i = 1; i <= joinMap.NumberOfPresets.JoinNumber; i++) { + int tempNum = i - 1; + string label = ""; + + var preset = presetsCamera.Presets.FirstOrDefault(p => p.ID.Equals(i)); + + if (preset != null) + label = preset.Description; + + trilist.SetString((ushort)(joinMap.PresetLabelStart.JoinNumber + tempNum), label); } } + } + + +public class CameraPreset : PresetBase +{ + public CameraPreset(int id, string description, bool isDefined, bool isDefinable) + : base(id, description, isDefined, isDefinable) + { + + } +} /// @@ -361,22 +290,12 @@ namespace PepperDash.Essentials.Devices.Common.Cameras /// public ControlPropertiesConfig Control { get; set; } - /// - /// Gets or sets the SupportsAutoMode - /// - [JsonProperty("supportsAutoMode")] - public bool SupportsAutoMode { get; set; } + [JsonProperty("supportsAutoMode")] + public bool SupportsAutoMode { get; set; } - /// - /// Gets or sets the SupportsOffMode - /// - [JsonProperty("supportsOffMode")] - public bool SupportsOffMode { get; set; } + [JsonProperty("supportsOffMode")] + public bool SupportsOffMode { get; set; } - /// - /// Gets or sets the Presets - /// - [JsonProperty("presets")] - public List Presets { get; set; } - } -} \ No newline at end of file + [JsonProperty("presets")] + public List Presets { get; set; } + } diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs index 126bcd27..e1ca7102 100644 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs +++ b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs @@ -12,833 +12,704 @@ using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Cameras +namespace PepperDash.Essentials.Devices.Common.Cameras; + +/// +/// Camera driver for cameras that use the VISCA control protocol. This driver supports both IP and RS-232 control depending on the communication method passed in. It also supports pan/tilt speed control, presets, and focus control. +/// +public class CameraVisca : CameraBase, IHasCameraPtzControl, ICommunicationMonitor, IHasCameraPresets, IHasPowerControlWithFeedback, IBridgeAdvanced, IHasCameraFocusControl, IHasAutoFocusMode { + private readonly CameraViscaPropertiesConfig PropertiesConfig; + /// - /// Represents a CameraVisca + /// Gets or sets the Communication /// - public class CameraVisca : CameraBase, IHasCameraPtzControl, ICommunicationMonitor, IHasCameraPresets, IHasPowerControlWithFeedback, IBridgeAdvanced, IHasCameraFocusControl, IHasAutoFocusMode + public IBasicCommunication Communication { get; private set; } + + /// + /// Gets or sets the CommunicationMonitor + /// + public StatusMonitorBase CommunicationMonitor { get; private set; } + + /// + /// Used to store the actions to parse inquiry responses as the inquiries are sent + /// + private readonly CrestronQueue> InquiryResponseQueue; + + /// + /// Camera ID (Default 1) + /// + public byte ID = 0x01; + public byte ResponseID; + + /// + /// Slow speed value for pan movement + /// + public byte PanSpeedSlow = 0x10; + + /// + /// Slow speed value for tilt movement + /// + public byte TiltSpeedSlow = 0x10; + + public byte PanSpeedFast = 0x13; + public byte TiltSpeedFast = 0x13; + + // private bool IsMoving; + private bool IsZooming; + + bool _powerIsOn; + public bool PowerIsOn { - private readonly CameraViscaPropertiesConfig PropertiesConfig; - - /// - /// Gets or sets the Communication - /// - public IBasicCommunication Communication { get; private set; } - - /// - /// Gets or sets the CommunicationMonitor - /// - public StatusMonitorBase CommunicationMonitor { get; private set; } - - /// - /// Used to store the actions to parse inquiry responses as the inquiries are sent - /// - private readonly CrestronQueue> InquiryResponseQueue; - - /// - /// Camera ID (Default 1) - /// - public byte ID = 0x01; - - /// - /// Response ID used for VISCA communication - /// - public byte ResponseID; - - /// - /// Slow speed value for pan movement - /// - public byte PanSpeedSlow = 0x10; - - /// - /// Slow speed value for tilt movement - /// - public byte TiltSpeedSlow = 0x10; - - /// - /// Fast speed value for pan movement - /// - public byte PanSpeedFast = 0x13; - - /// - /// Fast speed value for tilt movement - /// - public byte TiltSpeedFast = 0x13; - - // private bool IsMoving; - private bool IsZooming; - - bool _powerIsOn; - - /// - /// Gets or sets a value indicating whether the camera power is on - /// - public bool PowerIsOn + get { - get + return _powerIsOn; + } + private set + { + if (value != _powerIsOn) { - return _powerIsOn; - } - private set - { - if (value != _powerIsOn) - { - _powerIsOn = value; - PowerIsOnFeedback.FireUpdate(); - CameraIsOffFeedback.FireUpdate(); - } + _powerIsOn = value; + PowerIsOnFeedback.FireUpdate(); + CameraIsOffFeedback.FireUpdate(); } } + } - const byte ZoomInCmd = 0x02; - const byte ZoomOutCmd = 0x03; - const byte ZoomStopCmd = 0x00; + const byte ZoomInCmd = 0x02; + const byte ZoomOutCmd = 0x03; + const byte ZoomStopCmd = 0x00; - /// - /// Used to determine when to move the camera at a faster speed if a direction is held - /// - CTimer SpeedTimer; - // TODO: Implment speed timer for PTZ controls + /// + /// Used to determine when to move the camera at a faster speed if a direction is held + /// + CTimer SpeedTimer; + // TODO: Implment speed timer for PTZ controls - long FastSpeedHoldTimeMs = 2000; + long FastSpeedHoldTimeMs = 2000; - byte[] IncomingBuffer = new byte[] { }; + byte[] IncomingBuffer = new byte[] { }; - /// - /// Feedback indicating whether the camera power is on - /// - public BoolFeedback PowerIsOnFeedback { get; private set; } - - /// - /// Initializes a new instance of the CameraVisca class - /// - /// The unique key for this camera device - /// The friendly name for this camera device - /// The communication interface for VISCA protocol - /// The camera properties configuration - public CameraVisca(string key, string name, IBasicCommunication comm, CameraViscaPropertiesConfig props) : + public BoolFeedback PowerIsOnFeedback { get; private set; } + public CameraVisca(string key, string name, IBasicCommunication comm, CameraViscaPropertiesConfig props) : base(key, name) + { + InquiryResponseQueue = new CrestronQueue>(15); + + Presets = props.Presets; + + PropertiesConfig = props; + + ID = (byte)(props.Id + 0x80); + ResponseID = (byte)((props.Id * 0x10) + 0x80); + + SetupCameraSpeeds(); + + OutputPorts.Add(new RoutingOutputPort("videoOut", eRoutingSignalType.Video, eRoutingPortConnectionType.None, null, this, true)); + + // Default to all capabilties + Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus; + + Communication = comm; + if (comm is ISocketStatus socket) { - InquiryResponseQueue = new CrestronQueue>(15); - - Presets = props.Presets; - - PropertiesConfig = props; - - ID = (byte)(props.Id + 0x80); - ResponseID = (byte)((props.Id * 0x10) + 0x80); - - SetupCameraSpeeds(); - - OutputPorts.Add(new RoutingOutputPort("videoOut", eRoutingSignalType.Video, eRoutingPortConnectionType.None, null, this, true)); - - // Default to all capabilties - Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus; - - Communication = comm; - if (comm is ISocketStatus socket) - { - // This instance uses IP control - socket.ConnectionChange += new EventHandler(Socket_ConnectionChange); - } - else - { - // This instance uses RS-232 control - } - - Communication.BytesReceived += new EventHandler(Communication_BytesReceived); - PowerIsOnFeedback = new BoolFeedback("powerIsOn", () => { return PowerIsOn; }); - CameraIsOffFeedback = new BoolFeedback("cameraIsOff", () => { return !PowerIsOn; }); - - if (props.CommunicationMonitorProperties != null) - { - CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); - } - else - { - CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 20000, 120000, 300000, "\x81\x09\x04\x00\xFF"); - } - DeviceManager.AddDevice(CommunicationMonitor); + // This instance uses IP control + socket.ConnectionChange += new EventHandler(Socket_ConnectionChange); + } + else + { + // This instance uses RS-232 control } + Communication.BytesReceived += new EventHandler(Communication_BytesReceived); + PowerIsOnFeedback = new BoolFeedback(() => { return PowerIsOn; }); + CameraIsOffFeedback = new BoolFeedback(() => { return !PowerIsOn; }); - /// - /// Sets up camera speed values based on config - /// - void SetupCameraSpeeds() + if (props.CommunicationMonitorProperties != null) { - if (PropertiesConfig.FastSpeedHoldTimeMs > 0) - { - FastSpeedHoldTimeMs = PropertiesConfig.FastSpeedHoldTimeMs; - } + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); + } + else + { + CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 20000, 120000, 300000, "\x81\x09\x04\x00\xFF"); + } + DeviceManager.AddDevice(CommunicationMonitor); + } - if (PropertiesConfig.PanSpeedSlow > 0) - { - PanSpeedSlow = (byte)PropertiesConfig.PanSpeedSlow; - } - if (PropertiesConfig.PanSpeedFast > 0) - { - PanSpeedFast = (byte)PropertiesConfig.PanSpeedFast; - } - if (PropertiesConfig.TiltSpeedSlow > 0) - { - TiltSpeedSlow = (byte)PropertiesConfig.TiltSpeedSlow; - } - if (PropertiesConfig.TiltSpeedFast > 0) - { - TiltSpeedFast = (byte)PropertiesConfig.TiltSpeedFast; - } + /// + /// Sets up camera speed values based on config + /// + void SetupCameraSpeeds() + { + if (PropertiesConfig.FastSpeedHoldTimeMs > 0) + { + FastSpeedHoldTimeMs = PropertiesConfig.FastSpeedHoldTimeMs; } - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() + if (PropertiesConfig.PanSpeedSlow > 0) { - Communication.Connect(); - - - CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; - CommunicationMonitor.Start(); - - - CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); - return true; + PanSpeedSlow = (byte)PropertiesConfig.PanSpeedSlow; + } + if (PropertiesConfig.PanSpeedFast > 0) + { + PanSpeedFast = (byte)PropertiesConfig.PanSpeedFast; } - /// - /// LinkToApi method - /// - public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + if (PropertiesConfig.TiltSpeedSlow > 0) { - LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge); + TiltSpeedSlow = (byte)PropertiesConfig.TiltSpeedSlow; } - - void Socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + if (PropertiesConfig.TiltSpeedFast > 0) { - Debug.LogMessage(LogEventLevel.Verbose, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); - - if (e.Client.IsConnected) - { - - } - else - { - - } + TiltSpeedFast = (byte)PropertiesConfig.TiltSpeedFast; } + } + + public override bool CustomActivate() + { + Communication.Connect(); - void SendBytes(byte[] b) + CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + + CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; + CommunicationMonitor.Start(); + + + CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); + return true; + } + + /// + /// LinkToApi method + /// + public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge); + } + + void Socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); + + if (e.Client.IsConnected) { + } + else + { + + } + } + + + void SendBytes(byte[] b) + { + + if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 + Debug.LogMessage(LogEventLevel.Verbose, this, "Sending:{0}", ComTextHelper.GetEscapedText(b)); + + Communication.SendBytes(b); + } + + void Communication_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) + { + var newBytes = new byte[IncomingBuffer.Length + e.Bytes.Length]; + + try + { + // This is probably not thread-safe buffering + // Append the incoming bytes with whatever is in the buffer + IncomingBuffer.CopyTo(newBytes, 0); + e.Bytes.CopyTo(newBytes, IncomingBuffer.Length); if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 - Debug.LogMessage(LogEventLevel.Verbose, this, "Sending:{0}", ComTextHelper.GetEscapedText(b)); + Debug.LogMessage(LogEventLevel.Verbose, this, "Received:{0}", ComTextHelper.GetEscapedText(newBytes)); - Communication.SendBytes(b); - } + byte[] message = new byte[] { }; - void Communication_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) - { - var newBytes = new byte[IncomingBuffer.Length + e.Bytes.Length]; - - try + // Search for the delimiter 0xFF character + for (int i = 0; i < newBytes.Length; i++) { - // This is probably not thread-safe buffering - // Append the incoming bytes with whatever is in the buffer - IncomingBuffer.CopyTo(newBytes, 0); - e.Bytes.CopyTo(newBytes, IncomingBuffer.Length); - if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 - Debug.LogMessage(LogEventLevel.Verbose, this, "Received:{0}", ComTextHelper.GetEscapedText(newBytes)); - - byte[] message = new byte[] { }; - - // Search for the delimiter 0xFF character - for (int i = 0; i < newBytes.Length; i++) + if (newBytes[i] == 0xFF) { - if (newBytes[i] == 0xFF) - { - // i will be the index of the delmiter character - message = newBytes.Take(i).ToArray(); - // Skip over what we just took and save the rest for next time - newBytes = newBytes.Skip(i).ToArray(); - } + // i will be the index of the delmiter character + message = newBytes.Take(i).ToArray(); + // Skip over what we just took and save the rest for next time + newBytes = newBytes.Skip(i).ToArray(); + } + } + + if (message.Length > 0) + { + // Check for matching ID + if (message[0] != ResponseID) + { + return; } - if (message.Length > 0) + switch (message[1]) { - // Check for matching ID - if (message[0] != ResponseID) - { - return; - } + case 0x40: + { + // ACK received + Debug.LogMessage(LogEventLevel.Verbose, this, "ACK Received"); + break; + } + case 0x50: + { - switch (message[1]) - { - case 0x40: + if (message[2] == 0xFF) { - // ACK received - Debug.LogMessage(LogEventLevel.Verbose, this, "ACK Received"); - break; + // Completion received + Debug.LogMessage(LogEventLevel.Verbose, this, "Completion Received"); } - case 0x50: + else { - - if (message[2] == 0xFF) + // Inquiry response received. Dequeue the next response handler and invoke it + if (InquiryResponseQueue.Count > 0) { - // Completion received - Debug.LogMessage(LogEventLevel.Verbose, this, "Completion Received"); + var inquiryAction = InquiryResponseQueue.Dequeue(); + + inquiryAction.Invoke(message.Skip(2).ToArray()); } else { - // Inquiry response received. Dequeue the next response handler and invoke it - if (InquiryResponseQueue.Count > 0) - { - var inquiryAction = InquiryResponseQueue.Dequeue(); - - inquiryAction.Invoke(message.Skip(2).ToArray()); - } - else - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Response Queue is empty. Nothing to dequeue."); - } + Debug.LogMessage(LogEventLevel.Verbose, this, "Response Queue is empty. Nothing to dequeue."); } - - break; } - case 0x60: + + break; + } + case 0x60: + { + // Error message + + switch (message[2]) { - // Error message - - switch (message[2]) - { - case 0x01: - { - // Message Length Error - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Message Length Error"); - break; - } - case 0x02: - { - // Syntax Error - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Syntax Error"); - break; - } - case 0x03: - { - // Command Buffer Full - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command Buffer Full"); - break; - } - case 0x04: - { - // Command Cancelled - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command Cancelled"); - break; - } - case 0x05: - { - // No Socket - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: No Socket"); - break; - } - case 0x41: - { - // Command not executable - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command not executable"); - break; - } - } - break; + case 0x01: + { + // Message Length Error + Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Message Length Error"); + break; + } + case 0x02: + { + // Syntax Error + Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Syntax Error"); + break; + } + case 0x03: + { + // Command Buffer Full + Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command Buffer Full"); + break; + } + case 0x04: + { + // Command Cancelled + Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command Cancelled"); + break; + } + case 0x05: + { + // No Socket + Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: No Socket"); + break; + } + case 0x41: + { + // Command not executable + Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command not executable"); + break; + } } - } - - if (message == new byte[] { ResponseID, 0x50, 0x02, 0xFF }) - { - PowerIsOn = true; - } - else if (message == new byte[] { ResponseID, 0x50, 0x03, 0xFF }) - { - PowerIsOn = false; - } - + break; + } } - } - catch (Exception err) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Error parsing feedback: {0}", err); - } - finally - { - // Save whatever partial message is here - IncomingBuffer = newBytes; - } - } - - /// - /// Sends a pan/tilt command. If the command is not for fastSpeed then it starts a timer to initiate fast speed. - /// - /// The VISCA command to send - /// Whether fast speed is enabled for this command - private void SendPanTiltCommand(byte[] cmd, bool fastSpeedEnabled) - { - SendBytes(GetPanTiltCommand(cmd, fastSpeedEnabled)); - - if (!fastSpeedEnabled) - { - if (SpeedTimer != null) + if (message == new byte[] { ResponseID, 0x50, 0x02, 0xFF }) { - StopSpeedTimer(); + PowerIsOn = true; + } + else if (message == new byte[] { ResponseID, 0x50, 0x03, 0xFF }) + { + PowerIsOn = false; } - // Start the timer to send fast speed if still moving after FastSpeedHoldTime elapses - SpeedTimer = new CTimer((o) => SendPanTiltCommand(GetPanTiltCommand(cmd, true), true), FastSpeedHoldTimeMs); } } + catch (Exception err) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Error parsing feedback: {0}", err); + } + finally + { + // Save whatever partial message is here + IncomingBuffer = newBytes; + } + } - private void StopSpeedTimer() + /// + /// Sends a pan/tilt command. If the command is not for fastSpeed then it starts a timer to initiate fast speed. + /// + /// + /// + private void SendPanTiltCommand(byte[] cmd, bool fastSpeedEnabled) + { + SendBytes(GetPanTiltCommand(cmd, fastSpeedEnabled)); + + if (!fastSpeedEnabled) { if (SpeedTimer != null) - { - SpeedTimer.Stop(); - SpeedTimer.Dispose(); - SpeedTimer = null; - } - } - - /// - /// Generates the pan/tilt command with either slow or fast speed - /// - /// - /// - /// - private byte[] GetPanTiltCommand(byte[] cmd, bool fastSpeed) - { - byte panSpeed; - byte tiltSpeed; - - if (!fastSpeed) - { - panSpeed = PanSpeedSlow; - tiltSpeed = TiltSpeedSlow; - } - else - { - panSpeed = PanSpeedFast; - tiltSpeed = TiltSpeedFast; - } - - var temp = new byte[] { ID, 0x01, 0x06, 0x01, panSpeed, tiltSpeed }; - int length = temp.Length + cmd.Length + 1; - - byte[] sum = new byte[length]; - temp.CopyTo(sum, 0); - cmd.CopyTo(sum, temp.Length); - sum[length - 1] = 0xFF; - - return sum; - } - - - void SendPowerQuery() - { - SendBytes(new byte[] { ID, 0x09, 0x04, 0x00, 0xFF }); - InquiryResponseQueue.Enqueue(HandlePowerResponse); - } - - /// - /// PowerOn method - /// - public void PowerOn() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x00, 0x02, 0xFF }); - SendPowerQuery(); - } - - void HandlePowerResponse(byte[] response) - { - switch (response[0]) - { - case 0x02: - { - PowerIsOn = true; - break; - } - case 0x03: - { - PowerIsOn = false; - break; - } - } - } - - /// - /// PowerOff method - /// - public void PowerOff() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x00, 0x03, 0xFF }); - SendPowerQuery(); - } - - /// - /// PowerToggle method - /// - public void PowerToggle() - { - if (PowerIsOnFeedback.BoolValue) - PowerOff(); - else - PowerOn(); - } - - /// - /// PanLeft method - /// - public void PanLeft() - { - SendPanTiltCommand(new byte[] { 0x01, 0x03 }, false); - // IsMoving = true; - } - /// - /// PanRight method - /// - public void PanRight() - { - SendPanTiltCommand(new byte[] { 0x02, 0x03 }, false); - // IsMoving = true; - } - /// - /// PanStop method - /// - public void PanStop() - { - Stop(); - } - /// - /// TiltDown method - /// - public void TiltDown() - { - SendPanTiltCommand(new byte[] { 0x03, 0x02 }, false); - // IsMoving = true; - } - /// - /// TiltUp method - /// - public void TiltUp() - { - SendPanTiltCommand(new byte[] { 0x03, 0x01 }, false); - // IsMoving = true; - } - /// - /// TiltStop method - /// - public void TiltStop() - { - Stop(); - } - - private void SendZoomCommand(byte cmd) - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x07, cmd, 0xFF }); - } - - - /// - /// ZoomIn method - /// - public void ZoomIn() - { - SendZoomCommand(ZoomInCmd); - IsZooming = true; - } - /// - /// ZoomOut method - /// - public void ZoomOut() - { - SendZoomCommand(ZoomOutCmd); - IsZooming = true; - } - /// - /// ZoomStop method - /// - public void ZoomStop() - { - Stop(); - } - - /// - /// Stop method - /// - public void Stop() - { - if (IsZooming) - { - SendZoomCommand(ZoomStopCmd); - IsZooming = false; - } - else { StopSpeedTimer(); - SendPanTiltCommand(new byte[] { 0x03, 0x03 }, false); - // IsMoving = false; } - } - /// - /// PositionHome method - /// - public void PositionHome() - { - SendBytes(new byte[] { ID, 0x01, 0x06, 0x02, PanSpeedFast, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }); - SendBytes(new byte[] { ID, 0x01, 0x04, 0x47, 0x00, 0x00, 0x00, 0x00, 0xFF }); - } - /// - /// RecallPreset method - /// - public void RecallPreset(int presetNumber) - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x3F, 0x02, (byte)presetNumber, 0xFF }); - } - /// - /// SavePreset method - /// - public void SavePreset(int presetNumber) - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x3F, 0x01, (byte)presetNumber, 0xFF }); + + // Start the timer to send fast speed if still moving after FastSpeedHoldTime elapses + SpeedTimer = new CTimer((o) => SendPanTiltCommand(GetPanTiltCommand(cmd, true), true), FastSpeedHoldTimeMs); } - #region IHasCameraPresets Members + } - /// - /// Event that is raised when the presets list has changed - /// - public event EventHandler PresetsListHasChanged; - - /// - /// Raises the PresetsListHasChanged event - /// - protected void OnPresetsListHasChanged() + private void StopSpeedTimer() + { + if (SpeedTimer != null) { - var handler = PresetsListHasChanged; - if (handler == null) - return; + SpeedTimer.Stop(); + SpeedTimer.Dispose(); + SpeedTimer = null; + } + } - handler.Invoke(this, EventArgs.Empty); + /// + /// Generates the pan/tilt command with either slow or fast speed + /// + /// + /// + /// + private byte[] GetPanTiltCommand(byte[] cmd, bool fastSpeed) + { + byte panSpeed; + byte tiltSpeed; + + if (!fastSpeed) + { + panSpeed = PanSpeedSlow; + tiltSpeed = TiltSpeedSlow; + } + else + { + panSpeed = PanSpeedFast; + tiltSpeed = TiltSpeedFast; } - /// - /// Gets or sets the Presets - /// - public List Presets { get; private set; } + var temp = new byte[] { ID, 0x01, 0x06, 0x01, panSpeed, tiltSpeed }; + int length = temp.Length + cmd.Length + 1; - /// - /// PresetSelect method - /// - public void PresetSelect(int preset) + byte[] sum = new byte[length]; + temp.CopyTo(sum, 0); + cmd.CopyTo(sum, temp.Length); + sum[length - 1] = 0xFF; + + return sum; + } + + + void SendPowerQuery() + { + SendBytes(new byte[] { ID, 0x09, 0x04, 0x00, 0xFF }); + InquiryResponseQueue.Enqueue(HandlePowerResponse); + } + + public void PowerOn() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x00, 0x02, 0xFF }); + SendPowerQuery(); + } + + void HandlePowerResponse(byte[] response) + { + switch (response[0]) { - RecallPreset(preset); + case 0x02: + { + PowerIsOn = true; + break; + } + case 0x03: + { + PowerIsOn = false; + break; + } } + } - /// - /// PresetStore method - /// - public void PresetStore(int preset, string description) - { - SavePreset(preset); - } + public void PowerOff() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x00, 0x03, 0xFF }); + SendPowerQuery(); + } - - #endregion - - #region IHasCameraFocusControl Members - - /// - /// FocusNear method - /// - public void FocusNear() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x03, 0xFF }); - } - - /// - /// FocusFar method - /// - public void FocusFar() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x02, 0xFF }); - } - - /// - /// FocusStop method - /// - public void FocusStop() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x00, 0xFF }); - } - - /// - /// TriggerAutoFocus method - /// - public void TriggerAutoFocus() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x18, 0x01, 0xFF }); - SendAutoFocusQuery(); - } - - #endregion - - #region IHasAutoFocus Members - - /// - /// SetFocusModeAuto method - /// - public void SetFocusModeAuto() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x02, 0xFF }); - SendAutoFocusQuery(); - } - - /// - /// SetFocusModeManual method - /// - public void SetFocusModeManual() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x03, 0xFF }); - SendAutoFocusQuery(); - } - - /// - /// ToggleFocusMode method - /// - public void ToggleFocusMode() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x10, 0xFF }); - SendAutoFocusQuery(); - } - - #endregion - - void SendAutoFocusQuery() - { - SendBytes(new byte[] { ID, 0x09, 0x04, 0x38, 0xFF }); - InquiryResponseQueue.Enqueue(HandleAutoFocusResponse); - } - - void HandleAutoFocusResponse(byte[] response) - { - switch (response[0]) - { - case 0x02: - { - // Auto Mode - PowerIsOn = true; - break; - } - case 0x03: - { - // Manual Mode - PowerIsOn = false; - break; - } - } - } - - #region IHasCameraOff Members - - /// - /// Gets or sets the CameraIsOffFeedback - /// - public BoolFeedback CameraIsOffFeedback { get; private set; } - - - /// - /// CameraOff method - /// - public void CameraOff() - { + public void PowerToggle() + { + if (PowerIsOnFeedback.BoolValue) PowerOff(); - } - - #endregion + else + PowerOn(); } + public void PanLeft() + { + SendPanTiltCommand(new byte[] { 0x01, 0x03 }, false); + // IsMoving = true; + } + public void PanRight() + { + SendPanTiltCommand(new byte[] { 0x02, 0x03 }, false); + // IsMoving = true; + } + public void PanStop() + { + Stop(); + } + public void TiltDown() + { + SendPanTiltCommand(new byte[] { 0x03, 0x02 }, false); + // IsMoving = true; + } + public void TiltUp() + { + SendPanTiltCommand(new byte[] { 0x03, 0x01 }, false); + // IsMoving = true; + } + public void TiltStop() + { + Stop(); + } + + private void SendZoomCommand(byte cmd) + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x07, cmd, 0xFF }); + } + + + public void ZoomIn() + { + SendZoomCommand(ZoomInCmd); + IsZooming = true; + } + public void ZoomOut() + { + SendZoomCommand(ZoomOutCmd); + IsZooming = true; + } + public void ZoomStop() + { + Stop(); + } + + public void Stop() + { + if (IsZooming) + { + SendZoomCommand(ZoomStopCmd); + IsZooming = false; + } + else + { + StopSpeedTimer(); + SendPanTiltCommand(new byte[] { 0x03, 0x03 }, false); + // IsMoving = false; + } + } + public void PositionHome() + { + SendBytes(new byte[] { ID, 0x01, 0x06, 0x02, PanSpeedFast, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }); + SendBytes(new byte[] { ID, 0x01, 0x04, 0x47, 0x00, 0x00, 0x00, 0x00, 0xFF }); + } + public void RecallPreset(int presetNumber) + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x3F, 0x02, (byte)presetNumber, 0xFF }); + } + public void SavePreset(int presetNumber) + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x3F, 0x01, (byte)presetNumber, 0xFF }); + } + + #region IHasCameraPresets Members + + public event EventHandler PresetsListHasChanged; + /// - /// Represents a CameraViscaFactory + /// Raises the PresetsListHasChanged event /// - public class CameraViscaFactory : EssentialsDeviceFactory + protected void OnPresetsListHasChanged() { - /// - /// Initializes a new instance of the CameraViscaFactory class - /// - public CameraViscaFactory() - { - TypeNames = new List() { "cameravisca" }; - } + var handler = PresetsListHasChanged; + if (handler == null) + return; - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) + handler.Invoke(this, EventArgs.Empty); + } + + public List Presets { get; private set; } + + public void PresetSelect(int preset) + { + RecallPreset(preset); + } + + public void PresetStore(int preset, string description) + { + SavePreset(preset); + } + + + #endregion + + #region IHasCameraFocusControl Members + + public void FocusNear() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x03, 0xFF }); + } + + public void FocusFar() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x02, 0xFF }); + } + + public void FocusStop() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x00, 0xFF }); + } + + public void TriggerAutoFocus() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x18, 0x01, 0xFF }); + SendAutoFocusQuery(); + } + + #endregion + + #region IHasAutoFocus Members + + public void SetFocusModeAuto() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x02, 0xFF }); + SendAutoFocusQuery(); + } + + public void SetFocusModeManual() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x03, 0xFF }); + SendAutoFocusQuery(); + } + + public void ToggleFocusMode() + { + SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x10, 0xFF }); + SendAutoFocusQuery(); + } + + #endregion + + void SendAutoFocusQuery() + { + SendBytes(new byte[] { ID, 0x09, 0x04, 0x38, 0xFF }); + InquiryResponseQueue.Enqueue(HandleAutoFocusResponse); + } + + void HandleAutoFocusResponse(byte[] response) + { + switch (response[0]) { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new CameraVisca Device"); - var comm = CommFactory.CreateCommForDevice(dc); - var props = Newtonsoft.Json.JsonConvert.DeserializeObject( - dc.Properties.ToString()); - return new Cameras.CameraVisca(dc.Key, dc.Name, comm, props); + case 0x02: + { + // Auto Mode + PowerIsOn = true; + break; + } + case 0x03: + { + // Manual Mode + PowerIsOn = false; + break; + } } } + #region IHasCameraOff Members + + public BoolFeedback CameraIsOffFeedback { get; private set; } + + + public void CameraOff() + { + PowerOff(); + } + + #endregion +} + +public class CameraViscaFactory : EssentialsDeviceFactory +{ + public CameraViscaFactory() + { + TypeNames = new List() { "cameravisca" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new CameraVisca Device"); + var comm = CommFactory.CreateCommForDevice(dc); + var props = Newtonsoft.Json.JsonConvert.DeserializeObject( + dc.Properties.ToString()); + return new Cameras.CameraVisca(dc.Key, dc.Name, comm, props); + } +} + + +public class CameraViscaPropertiesConfig : CameraPropertiesConfig +{ + /// + /// Control ID of the camera (1-7) + /// + [JsonProperty("id")] + public uint Id { get; set; } /// - /// Represents a CameraViscaPropertiesConfig + /// Slow Pan speed (0-18) /// - public class CameraViscaPropertiesConfig : CameraPropertiesConfig - { - /// - /// Control ID of the camera (1-7) - /// Gets or sets the Id - /// - [JsonProperty("id")] - public uint Id { get; set; } + [JsonProperty("panSpeedSlow")] + public uint PanSpeedSlow { get; set; } - /// - /// Slow Pan speed (0-18) - /// - [JsonProperty("panSpeedSlow")] - public uint PanSpeedSlow { get; set; } + /// + /// Fast Pan speed (0-18) + /// + [JsonProperty("panSpeedFast")] + public uint PanSpeedFast { get; set; } - /// - /// Fast Pan speed (0-18) - /// - [JsonProperty("panSpeedFast")] - public uint PanSpeedFast { get; set; } + /// + /// Slow tilt speed (0-18) + /// + [JsonProperty("tiltSpeedSlow")] + public uint TiltSpeedSlow { get; set; } - /// - /// Slow tilt speed (0-18) - /// - [JsonProperty("tiltSpeedSlow")] - public uint TiltSpeedSlow { get; set; } + /// + /// Fast tilt speed (0-18) + /// + [JsonProperty("tiltSpeedFast")] + public uint TiltSpeedFast { get; set; } - /// - /// Fast tilt speed (0-18) - /// - [JsonProperty("tiltSpeedFast")] - public uint TiltSpeedFast { get; set; } - - /// - /// Time a button must be held before fast speed is engaged (Milliseconds) - /// - [JsonProperty("fastSpeedHoldTimeMs")] - public uint FastSpeedHoldTimeMs { get; set; } - - } + /// + /// Time a button must be held before fast speed is engaged (Milliseconds) + /// + [JsonProperty("fastSpeedHoldTimeMs")] + public uint FastSpeedHoldTimeMs { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/IHasCameraPresets.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/IHasCameraPresets.cs index 0234a405..2fcdb9aa 100644 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/IHasCameraPresets.cs +++ b/src/PepperDash.Essentials.Devices.Common/Cameras/IHasCameraPresets.cs @@ -1,34 +1,34 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Devices.Common.Cameras +namespace PepperDash.Essentials.Devices.Common.Cameras; + + +/// +/// Describes a camera with preset functionality +/// +public interface IHasCameraPresets { /// - /// Describes a camera with preset functionality + /// Event that is raised when the presets list has changed /// - public interface IHasCameraPresets - { - /// - /// Event that is raised when the presets list has changed - /// - event EventHandler PresetsListHasChanged; + event EventHandler PresetsListHasChanged; - /// - /// Gets the list of camera presets - /// - List Presets { get; } + /// + /// Gets the list of camera presets + /// + List Presets { get; } - /// - /// Selects the specified preset - /// - /// The preset number to select - void PresetSelect(int preset); + /// + /// Selects the specified preset + /// + /// The preset number to select + void PresetSelect(int preset); - /// - /// Stores a preset at the specified location with the given description - /// - /// The preset number to store - /// The description for the preset - void PresetStore(int preset, string description); - } -} \ No newline at end of file + /// + /// Stores a preset at the specified location with the given description + /// + /// The preset number to store + /// The description for the preset + void PresetStore(int preset, string description); +} diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/Cisco/IPresenterTrack.cs b/src/PepperDash.Essentials.Devices.Common/Codec/Cisco/IPresenterTrack.cs index 134dc7b5..b98cb4e7 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/Cisco/IPresenterTrack.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/Cisco/IPresenterTrack.cs @@ -1,90 +1,89 @@ using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.Codec.Cisco +namespace PepperDash.Essentials.Devices.Common.Codec.Cisco; + +/// +/// Describes the available tracking modes for a Cisco codec's Presenter Track feature. +/// +public enum ePresenterTrackMode { /// - /// Describes the available tracking modes for a Cisco codec's Presenter Track feature. + /// Presenter Track is turned off. /// - public enum ePresenterTrackMode - { - /// - /// Presenter Track is turned off. - /// - Off, - /// - /// Presenter Track follows the speaker's movements. - /// - Follow, - /// - /// Presenter Track is set to background mode, where it tracks the speaker but does not actively follow. - /// - Background, - /// - /// Presenter Track is set to persistent mode, where it maintains a fixed position or focus on the speaker. - /// - Persistent - } + Off, + /// + /// Presenter Track follows the speaker's movements. + /// + Follow, + /// + /// Presenter Track is set to background mode, where it tracks the speaker but does not actively follow. + /// + Background, + /// + /// Presenter Track is set to persistent mode, where it maintains a fixed position or focus on the speaker. + /// + Persistent +} +/// +/// Describes the Presenter Track controls for a Cisco codec. +/// +public interface IPresenterTrack : IKeyed +{ + /// + /// + /// + bool PresenterTrackAvailability { get; } + /// - /// Describes the Presenter Track controls for a Cisco codec. + /// Feedback indicating whether Presenter Track is available. /// - public interface IPresenterTrack : IKeyed - { - /// - /// - /// - bool PresenterTrackAvailability { get; } + BoolFeedback PresenterTrackAvailableFeedback { get; } - /// - /// Feedback indicating whether Presenter Track is available. - /// - BoolFeedback PresenterTrackAvailableFeedback { get; } + /// + /// Feedback indicating the current status of Presenter Track is off + /// + BoolFeedback PresenterTrackStatusOffFeedback { get; } - /// - /// Feedback indicating the current status of Presenter Track is off - /// - BoolFeedback PresenterTrackStatusOffFeedback { get; } + /// + /// Feedback indicating the current status of Presenter Track is follow + /// + BoolFeedback PresenterTrackStatusFollowFeedback { get; } - /// - /// Feedback indicating the current status of Presenter Track is follow - /// - BoolFeedback PresenterTrackStatusFollowFeedback { get; } + /// + /// Feedback indicating the current status of Presenter Track is background + /// + BoolFeedback PresenterTrackStatusBackgroundFeedback { get; } - /// - /// Feedback indicating the current status of Presenter Track is background - /// - BoolFeedback PresenterTrackStatusBackgroundFeedback { get; } + /// + /// Feedback indicating the current status of Presenter Track is persistent + /// + BoolFeedback PresenterTrackStatusPersistentFeedback { get; } - /// - /// Feedback indicating the current status of Presenter Track is persistent - /// - BoolFeedback PresenterTrackStatusPersistentFeedback { get; } + /// + /// Indicates the current status of Presenter Track. + /// + bool PresenterTrackStatus { get; } - /// - /// Indicates the current status of Presenter Track. - /// - bool PresenterTrackStatus { get; } + /// + /// Turns off Presenter Track. + /// + void PresenterTrackOff(); - /// - /// Turns off Presenter Track. - /// - void PresenterTrackOff(); + /// + /// Turns on Presenter Track in follow mode. + /// + void PresenterTrackFollow(); - /// - /// Turns on Presenter Track in follow mode. - /// - void PresenterTrackFollow(); + /// + /// Turns on Presenter Track in background mode. + /// + void PresenterTrackBackground(); - /// - /// Turns on Presenter Track in background mode. - /// - void PresenterTrackBackground(); - - /// - /// Turns on Presenter Track in persistent mode. - /// - void PresenterTrackPersistent(); - } + /// + /// Turns on Presenter Track in persistent mode. + /// + void PresenterTrackPersistent(); } diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/Cisco/ISpeakerTrack.cs b/src/PepperDash.Essentials.Devices.Common/Codec/Cisco/ISpeakerTrack.cs index 522e6ad2..5e4726b2 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/Cisco/ISpeakerTrack.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/Cisco/ISpeakerTrack.cs @@ -1,35 +1,34 @@ using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.Codec.Cisco +namespace PepperDash.Essentials.Devices.Common.Codec.Cisco; + +/// +/// Describes the available tracking modes for a Cisco codec +/// +public interface ISpeakerTrack : IKeyed { /// - /// Describes the available tracking modes for a Cisco codec + /// Indicates whether Speaker Track is available on the codec. /// - public interface ISpeakerTrack : IKeyed - { - /// - /// Indicates whether Speaker Track is available on the codec. - /// - bool SpeakerTrackAvailability { get; } + bool SpeakerTrackAvailability { get; } - /// - /// - /// - BoolFeedback SpeakerTrackAvailableFeedback { get; } + /// + /// + /// + BoolFeedback SpeakerTrackAvailableFeedback { get; } - /// - /// Feedback indicating the current status of Speaker Track is off - /// - bool SpeakerTrackStatus { get; } + /// + /// Feedback indicating the current status of Speaker Track is off + /// + bool SpeakerTrackStatus { get; } - /// - /// Turns Speaker Track off - /// - void SpeakerTrackOff(); - /// - /// Turns Speaker Track on - /// - void SpeakerTrackOn(); - } + /// + /// Turns Speaker Track off + /// + void SpeakerTrackOff(); + /// + /// Turns Speaker Track on + /// + void SpeakerTrackOn(); } diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/CodecActiveCallItem.cs b/src/PepperDash.Essentials.Devices.Common/Codec/CodecActiveCallItem.cs index 23a39f29..b10e816b 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/CodecActiveCallItem.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/CodecActiveCallItem.cs @@ -1,105 +1,100 @@ - - -using System; - +using System; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; +/// +/// Represents an active call item for a codec, including details such as name, number, type, status, direction, and duration. +/// +public class CodecActiveCallItem { /// - /// Represents a CodecActiveCallItem + /// The name associated with the call, if available. /// - public class CodecActiveCallItem - { - /// - /// Gets or sets the Name - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - /// - /// Gets or sets the Number - /// - [JsonProperty("number", NullValueHandling = NullValueHandling.Ignore)] - public string Number { get; set; } - - /// - /// Gets or sets the Type - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(StringEnumConverter))] - public eCodecCallType Type { get; set; } - - /// - /// Gets or sets the Status - /// - [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(StringEnumConverter))] - public eCodecCallStatus Status { get; set; } - - /// - /// Gets or sets the Direction - /// - [JsonProperty("direction", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(StringEnumConverter))] - public eCodecCallDirection Direction { get; set; } - - /// - /// Gets or sets the Id - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public string Id { get; set; } - - /// - /// Gets or sets the IsOnHold - /// - [JsonProperty("isOnHold", NullValueHandling = NullValueHandling.Ignore)] - public bool IsOnHold { get; set; } - - /// - /// Gets or sets the Duration - /// - [JsonProperty("duration", NullValueHandling = NullValueHandling.Ignore)] - public TimeSpan Duration { get; set; } - - //public object CallMetaData { get; set; } - - /// - /// Returns true when this call is any status other than - /// Unknown, Disconnected, Disconnecting - /// - [JsonProperty("isActiveCall", NullValueHandling = NullValueHandling.Ignore)] - public bool IsActiveCall - { - get - { - return !(Status == eCodecCallStatus.Disconnected - || Status == eCodecCallStatus.Disconnecting - || Status == eCodecCallStatus.Idle - || Status == eCodecCallStatus.Unknown); - } - } - } + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } /// - /// Represents a CodecCallStatusItemChangeEventArgs + /// The phone number associated with the call, if available. /// - public class CodecCallStatusItemChangeEventArgs : EventArgs - { - /// - /// Gets or sets the CallItem - /// - public CodecActiveCallItem CallItem { get; private set; } + [JsonProperty("number", NullValueHandling = NullValueHandling.Ignore)] + public string Number { get; set; } - /// - /// Initializes a new instance of the CodecCallStatusItemChangeEventArgs class - /// - /// The call item that changed - public CodecCallStatusItemChangeEventArgs(CodecActiveCallItem item) + /// + /// The type of the call, such as audio or video. This is an optional property and may be null if the type is unknown or not applicable. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(StringEnumConverter))] + public eCodecCallType Type { get; set; } + + /// + /// The current status of the call, such as active, on hold, or disconnected. This is an optional property and may be null if the status is unknown or not applicable. + /// + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(StringEnumConverter))] + public eCodecCallStatus Status { get; set; } + + /// + /// The direction of the call, such as incoming or outgoing. This is an optional property and may be null if the direction is unknown or not applicable. + /// + [JsonProperty("direction", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(StringEnumConverter))] + public eCodecCallDirection Direction { get; set; } + + /// + /// A unique identifier for the call, if available. This can be used to track the call across different events and status changes. This is an optional property and may be null if an identifier is not available or applicable. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; set; } + + /// + /// Indicates whether the call is currently on hold. This is an optional property and may be null if the hold status is unknown or not applicable. + /// + [JsonProperty("isOnHold", NullValueHandling = NullValueHandling.Ignore)] + public bool IsOnHold { get; set; } + + /// + /// The duration of the call, if available. This can be used to track how long the call has been active. This is an optional property and may be null if the duration is not available or applicable. + /// + [JsonProperty("duration", NullValueHandling = NullValueHandling.Ignore)] + public TimeSpan Duration { get; set; } + + //public object CallMetaData { get; set; } + + /// + /// Returns true when this call is any status other than + /// Unknown, Disconnected, Disconnecting + /// + [JsonProperty("isActiveCall", NullValueHandling = NullValueHandling.Ignore)] + public bool IsActiveCall + { + get { - CallItem = item; + return !(Status == eCodecCallStatus.Disconnected + || Status == eCodecCallStatus.Disconnecting + || Status == eCodecCallStatus.Idle + || Status == eCodecCallStatus.Unknown); } } +} + +/// +/// Event args for when a call status changes, includes the call item with updated status and details. +/// +public class CodecCallStatusItemChangeEventArgs : EventArgs +{ + /// + /// The call item that has changed status, including its updated status and details. This property is read-only and is set through the constructor when the event args are created. + /// + public CodecActiveCallItem CallItem { get; private set; } + + /// + /// Constructor for CodecCallStatusItemChangeEventArgs + /// + /// The call item that has changed status. + public CodecCallStatusItemChangeEventArgs(CodecActiveCallItem item) + { + CallItem = item; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/IHasCallHold.cs b/src/PepperDash.Essentials.Devices.Common/Codec/IHasCallHold.cs index 1445bd61..dddc7a6b 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/IHasCallHold.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/IHasCallHold.cs @@ -1,20 +1,20 @@ -namespace PepperDash.Essentials.Devices.Common.Codec + +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Defines the contract for devices that have call hold functionality +/// +public interface IHasCallHold { /// - /// Defines the contract for IHasCallHold + /// Put the specified call on hold /// - public interface IHasCallHold - { - /// - /// Put the specified call on hold - /// - /// - void HoldCall(CodecActiveCallItem activeCall); + /// The call to put on hold + void HoldCall(CodecActiveCallItem activeCall); - /// - /// Resume the specified call - /// - /// - void ResumeCall(CodecActiveCallItem activeCall); - } + /// + /// Resume the specified call + /// + /// The call to resume + void ResumeCall(CodecActiveCallItem activeCall); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/IHasDoNotDisturb.cs b/src/PepperDash.Essentials.Devices.Common/Codec/IHasDoNotDisturb.cs index c989e671..4cb46ec7 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/IHasDoNotDisturb.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/IHasDoNotDisturb.cs @@ -1,42 +1,41 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Describes a device that has Do Not Disturb mode capability +/// +public interface IHasDoNotDisturbMode { /// - /// Describes a device that has Do Not Disturb mode capability + /// Indictes whether Do Not Disturb mode is on (Activated) /// - public interface IHasDoNotDisturbMode - { - /// - /// Indictes whether Do Not Disturb mode is on (Activated) - /// - BoolFeedback DoNotDisturbModeIsOnFeedback { get; } - - /// - /// Activates Do Not Disturb mode - /// - void ActivateDoNotDisturbMode(); - - /// - /// Deactivates Do Not Disturb mode - /// - void DeactivateDoNotDisturbMode(); - - /// - /// Toggles Do Not Disturb mode - /// - void ToggleDoNotDisturbMode(); - } + BoolFeedback DoNotDisturbModeIsOnFeedback { get; } /// - /// Defines the contract for devices that support Do Not Disturb mode with timeout functionality + /// Activates Do Not Disturb mode /// - public interface IHasDoNotDisturbModeWithTimeout : IHasDoNotDisturbMode - { - /// - /// Activates Do Not Disturb mode with a timeout - /// - /// - void ActivateDoNotDisturbMode(int timeout); - } -} \ No newline at end of file + void ActivateDoNotDisturbMode(); + + /// + /// Deactivates Do Not Disturb mode + /// + void DeactivateDoNotDisturbMode(); + + /// + /// Toggles Do Not Disturb mode + /// + void ToggleDoNotDisturbMode(); +} + +/// +/// Defines the contract for devices that support Do Not Disturb mode with timeout functionality +/// +public interface IHasDoNotDisturbModeWithTimeout : IHasDoNotDisturbMode +{ + /// + /// Activates Do Not Disturb mode with a timeout + /// + /// + void ActivateDoNotDisturbMode(int timeout); +} diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/IHasExternalSourceSwitching.cs b/src/PepperDash.Essentials.Devices.Common/Codec/IHasExternalSourceSwitching.cs index 73884a2a..72742c2d 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/IHasExternalSourceSwitching.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/IHasExternalSourceSwitching.cs @@ -1,54 +1,54 @@ using System; using PepperDash.Essentials.Devices.Common.VideoCodec.Cisco; -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Defines the contract for devices that support external source switching functionality +/// +public interface IHasExternalSourceSwitching { /// - /// Defines the contract for IHasExternalSourceSwitching + /// Gets a value indicating whether the external source list is enabled /// - public interface IHasExternalSourceSwitching - { - /// - /// Gets a value indicating whether the external source list is enabled - /// - bool ExternalSourceListEnabled { get; } + bool ExternalSourceListEnabled { get; } - /// - /// Gets the external source input port identifier - /// - string ExternalSourceInputPort { get; } + /// + /// Gets the port name for external source input switching + /// + string ExternalSourceInputPort { get; } - /// - /// Adds an external source to the available sources - /// - /// The connector identifier - /// The unique key for the source - /// The display name for the source - /// The type of external source - void AddExternalSource(string connectorId, string key, string name, eExternalSourceType type); - /// - /// Sets the state of the specified external source - /// - /// The unique key of the external source - /// The mode to set for the source - void SetExternalSourceState(string key, eExternalSourceMode mode); + /// + /// Adds an external source to the list of available sources for switching + /// + /// + /// + /// + /// + void AddExternalSource(string connectorId, string key, string name, eExternalSourceType type); - /// - /// Clears all external sources from the list - /// - void ClearExternalSources(); + /// + /// Sets the state of the specified external source + /// + /// The unique key of the external source + /// The mode to set for the source + void SetExternalSourceState(string key, eExternalSourceMode mode); - /// - /// Sets the selected source by its key - /// - /// The unique key of the source to select - void SetSelectedSource(string key); + /// + /// Clears all external sources from the list + /// + void ClearExternalSources(); - /// - /// Sets the action to run when routing between sources - /// - Action RunRouteAction { set; } - } + /// + /// Sets the selected external source for switching + /// + /// + void SetSelectedSource(string key); -} \ No newline at end of file + /// + /// Defines an action to run when an external source is selected for switching. + /// The action takes the key and name of the selected source as parameters. + /// + Action RunRouteAction { set; } +} diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallDirection.cs b/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallDirection.cs index e10f61a3..89c852ca 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallDirection.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallDirection.cs @@ -1,57 +1,57 @@ -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Enumeration of eCodecCallDirection values +/// +public enum eCodecCallDirection { /// - /// Enumeration of eCodecCallDirection values + /// Unknown call direction /// - public enum eCodecCallDirection - { - /// - /// Unknown call direction - /// - Unknown = 0, - - /// - /// Incoming call direction - /// - Incoming, - - /// - /// Outgoing call direction - /// - Outgoing - } + Unknown = 0, /// - /// Represents a CodecCallDirection + /// Incoming call direction /// - public class CodecCallDirection - { - /// - /// Takes the Cisco call type and converts to the matching enum - /// - /// - /// - /// - /// ConvertToDirectionEnum method - /// - public static eCodecCallDirection ConvertToDirectionEnum(string s) - { - switch (s.ToLower()) - { - case "incoming": - { - return eCodecCallDirection.Incoming; - } - case "outgoing": - { - return eCodecCallDirection.Outgoing; - } - default: - return eCodecCallDirection.Unknown; - } + Incoming, + /// + /// Outgoing call direction + /// + Outgoing +} + +/// +/// Represents a CodecCallDirection +/// +public class CodecCallDirection +{ + /// + /// Takes the Cisco call type and converts to the matching enum + /// + /// + /// + /// + /// ConvertToDirectionEnum method + /// + public static eCodecCallDirection ConvertToDirectionEnum(string s) + { + switch (s.ToLower()) + { + case "incoming": + { + return eCodecCallDirection.Incoming; + } + case "outgoing": + { + return eCodecCallDirection.Outgoing; + } + default: + return eCodecCallDirection.Unknown; } } -} \ No newline at end of file + +} + diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallStatus.cs b/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallStatus.cs index 6116ab16..e9cc89d0 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallStatus.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallStatus.cs @@ -1,139 +1,138 @@ -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Enumeration of eCodecCallStatus values +/// +public enum eCodecCallStatus { /// - /// Enumeration of eCodecCallStatus values + /// Unknown call status /// - public enum eCodecCallStatus - { - /// - /// Unknown call status - /// - Unknown = 0, - - /// - /// Call is connected - /// - Connected, - - /// - /// Call is connecting - /// - Connecting, - - /// - /// Call is dialing - /// - Dialing, - - /// - /// Call is disconnected - /// - Disconnected, - - /// - /// Call is disconnecting - /// - Disconnecting, - - /// - /// Early media is being sent/received - /// - EarlyMedia, - - /// - /// Call is idle - /// - Idle, - - /// - /// Call is on hold - /// - OnHold, - - /// - /// Call is ringing - /// - Ringing, - - /// - /// Call is preserved - /// - Preserved, - - /// - /// Call is remote preserved - /// - RemotePreserved, - } - + Unknown = 0, /// - /// Represents a CodecCallStatus + /// Call is connected /// - public class CodecCallStatus + Connected, + + /// + /// Call is connecting + /// + Connecting, + + /// + /// Call is dialing + /// + Dialing, + + /// + /// Call is disconnected + /// + Disconnected, + + /// + /// Call is disconnecting + /// + Disconnecting, + + /// + /// Early media is being sent/received + /// + EarlyMedia, + + /// + /// Call is idle + /// + Idle, + + /// + /// Call is on hold + /// + OnHold, + + /// + /// Call is ringing + /// + Ringing, + + /// + /// Call is preserved + /// + Preserved, + + /// + /// Call is remote preserved + /// + RemotePreserved, +} + + +/// +/// Represents a CodecCallStatus +/// +public class CodecCallStatus +{ + + /// + /// Takes the Cisco call type and converts to the matching enum + /// + /// + /// + /// + /// ConvertToStatusEnum method + /// + public static eCodecCallStatus ConvertToStatusEnum(string s) { - - /// - /// Takes the Cisco call type and converts to the matching enum - /// - /// - /// - /// - /// ConvertToStatusEnum method - /// - public static eCodecCallStatus ConvertToStatusEnum(string s) + switch (s) { - switch (s) - { - case "Connected": - { - return eCodecCallStatus.Connected; - } - case "Connecting": - { - return eCodecCallStatus.Connecting; - } - case "Dialling": - { - return eCodecCallStatus.Dialing; - } - case "Disconnected": - { - return eCodecCallStatus.Disconnected; - } - case "Disconnecting": - { - return eCodecCallStatus.Disconnecting; - } - case "EarlyMedia": - { - return eCodecCallStatus.EarlyMedia; - } - case "Idle": - { - return eCodecCallStatus.Idle; - } - case "OnHold": - { - return eCodecCallStatus.OnHold; - } - case "Ringing": - { - return eCodecCallStatus.Ringing; - } - case "Preserved": - { - return eCodecCallStatus.Preserved; - } - case "RemotePreserved": - { - return eCodecCallStatus.RemotePreserved; - } - default: - return eCodecCallStatus.Unknown; - } - + case "Connected": + { + return eCodecCallStatus.Connected; + } + case "Connecting": + { + return eCodecCallStatus.Connecting; + } + case "Dialling": + { + return eCodecCallStatus.Dialing; + } + case "Disconnected": + { + return eCodecCallStatus.Disconnected; + } + case "Disconnecting": + { + return eCodecCallStatus.Disconnecting; + } + case "EarlyMedia": + { + return eCodecCallStatus.EarlyMedia; + } + case "Idle": + { + return eCodecCallStatus.Idle; + } + case "OnHold": + { + return eCodecCallStatus.OnHold; + } + case "Ringing": + { + return eCodecCallStatus.Ringing; + } + case "Preserved": + { + return eCodecCallStatus.Preserved; + } + case "RemotePreserved": + { + return eCodecCallStatus.RemotePreserved; + } + default: + return eCodecCallStatus.Unknown; } } -} \ No newline at end of file +} + diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallType.cs b/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallType.cs index 2b9609fb..947d0ee2 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallType.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/eCodecCallType.cs @@ -1,76 +1,75 @@ -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Enumeration of eCodecCallType values +/// +public enum eCodecCallType { /// - /// Enumeration of eCodecCallType values + /// Unknown call type /// - public enum eCodecCallType - { - /// - /// Unknown call type - /// - Unknown = 0, - - /// - /// Audio-only call type - /// - Audio, - - /// - /// Video call type - /// - Video, - - /// - /// Audio call that can be escalated to video - /// - AudioCanEscalate, - - /// - /// Forward all call type - /// - ForwardAllCall - } + Unknown = 0, /// - /// Represents a CodecCallType + /// Audio-only call type /// - public class CodecCallType + Audio, + + /// + /// Video call type + /// + Video, + + /// + /// Audio call that can be escalated to video + /// + AudioCanEscalate, + + /// + /// Forward all call type + /// + ForwardAllCall +} + +/// +/// Represents a CodecCallType +/// +public class CodecCallType +{ + + /// + /// Takes the Cisco call type and converts to the matching enum + /// + /// + /// + /// + /// ConvertToTypeEnum method + /// + public static eCodecCallType ConvertToTypeEnum(string s) { - - /// - /// Takes the Cisco call type and converts to the matching enum - /// - /// - /// - /// - /// ConvertToTypeEnum method - /// - public static eCodecCallType ConvertToTypeEnum(string s) + switch (s) { - switch (s) - { - case "Audio": - { - return eCodecCallType.Audio; - } - case "Video": - { - return eCodecCallType.Video; - } - case "AudioCanEscalate": - { - return eCodecCallType.AudioCanEscalate; - } - case "ForwardAllCall": - { - return eCodecCallType.ForwardAllCall; - } - default: - return eCodecCallType.Unknown; - } - + case "Audio": + { + return eCodecCallType.Audio; + } + case "Video": + { + return eCodecCallType.Video; + } + case "AudioCanEscalate": + { + return eCodecCallType.AudioCanEscalate; + } + case "ForwardAllCall": + { + return eCodecCallType.ForwardAllCall; + } + default: + return eCodecCallType.Unknown; } } + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/eMeetingPrivacy.cs b/src/PepperDash.Essentials.Devices.Common/Codec/eMeetingPrivacy.cs index 19a6d31b..b805b738 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/eMeetingPrivacy.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/eMeetingPrivacy.cs @@ -1,56 +1,55 @@ -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Enumeration of eMeetingPrivacy values +/// +public enum eMeetingPrivacy { /// - /// Enumeration of eMeetingPrivacy values + /// Unknown meeting privacy level /// - public enum eMeetingPrivacy - { - /// - /// Unknown meeting privacy level - /// - Unknown = 0, - - /// - /// Public meeting - /// - Public, - - /// - /// Private meeting - /// - Private - } + Unknown = 0, /// - /// Represents a CodecCallPrivacy + /// Public meeting /// - public class CodecCallPrivacy - { - /// - /// Takes the Cisco privacy type and converts to the matching enum - /// - /// - /// - /// - /// ConvertToDirectionEnum method - /// - public static eMeetingPrivacy ConvertToDirectionEnum(string s) - { - switch (s.ToLower()) - { - case "public": - { - return eMeetingPrivacy.Public; - } - case "private": - { - return eMeetingPrivacy.Private; - } - default: - return eMeetingPrivacy.Unknown; - } + Public, + /// + /// Private meeting + /// + Private +} + +/// +/// Represents a CodecCallPrivacy +/// +public class CodecCallPrivacy +{ + /// + /// Takes the Cisco privacy type and converts to the matching enum + /// + /// + /// + /// + /// ConvertToDirectionEnum method + /// + public static eMeetingPrivacy ConvertToDirectionEnum(string s) + { + switch (s) + { + case "public": + { + return eMeetingPrivacy.Public; + } + case "private": + { + return eMeetingPrivacy.Private; + } + default: + return eMeetingPrivacy.Unknown; } } + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/iCodecAudio.cs b/src/PepperDash.Essentials.Devices.Common/Codec/iCodecAudio.cs index eeab2801..0aa848a4 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/iCodecAudio.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/iCodecAudio.cs @@ -1,12 +1,11 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.Codec -{ - /// - /// Defines minimum volume controls for a codec device with dialing capabilities - /// - public interface ICodecAudio : IBasicVolumeWithFeedback, IPrivacy - { +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Defines minimum volume controls for a codec device with dialing capabilities +/// +public interface ICodecAudio : IBasicVolumeWithFeedback, IPrivacy +{ - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/iHasCallFavorites.cs b/src/PepperDash.Essentials.Devices.Common/Codec/iHasCallFavorites.cs index eeb6610f..b28b48cf 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/iHasCallFavorites.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/iHasCallFavorites.cs @@ -1,34 +1,34 @@ using System.Collections.Generic; -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + + +/// +/// Defines the contract for IHasCallFavorites +/// +public interface IHasCallFavorites { /// - /// Defines the contract for IHasCallFavorites + /// Gets the call favorites for this device /// - public interface IHasCallFavorites - { - /// - /// Gets the call favorites for this device - /// - CodecCallFavorites CallFavorites { get; } - } + CodecCallFavorites CallFavorites { get; } +} + +/// +/// Represents a CodecCallFavorites +/// +public class CodecCallFavorites +{ + /// + /// Gets or sets the Favorites + /// + public List Favorites { get; set; } /// - /// Represents a CodecCallFavorites + /// Initializes a new instance of the CodecCallFavorites class /// - public class CodecCallFavorites + public CodecCallFavorites() { - /// - /// Gets or sets the Favorites - /// - public List Favorites { get; set; } - - /// - /// Initializes a new instance of the CodecCallFavorites class - /// - public CodecCallFavorites() - { - Favorites = new List(); - } + Favorites = new List(); } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/iHasCallHistory.cs b/src/PepperDash.Essentials.Devices.Common/Codec/iHasCallHistory.cs index 7f495daa..025e4a12 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/iHasCallHistory.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/iHasCallHistory.cs @@ -6,188 +6,131 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Essentials.Devices.Common.VideoCodec; -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + + +public interface IHasCallHistory { + CodecCallHistory CallHistory { get; } + + void RemoveCallHistoryEntry(CodecCallHistory.CallHistoryEntry entry); +} + +public enum eCodecOccurrenceType +{ + Unknown = 0, + Placed = 1, + Received = 2, + NoAnswer = 3, +} + +/// +/// Represents the recent call history for a codec device +/// +public class CodecCallHistory +{ + public event EventHandler RecentCallsListHasChanged; + + public List RecentCalls { get; private set; } /// - /// Defines the contract for IHasCallHistory + /// Item that gets added to the list when there are no recent calls in history /// - public interface IHasCallHistory - { - /// - /// Gets the call history for this device - /// - CodecCallHistory CallHistory { get; } + CallHistoryEntry ListEmptyEntry; - /// - /// Removes the specified call history entry - /// - /// The call history entry to remove - void RemoveCallHistoryEntry(CodecCallHistory.CallHistoryEntry entry); + public CodecCallHistory() + { + ListEmptyEntry = new CallHistoryEntry() { Name = "No Recent Calls" }; + + RecentCalls = new List(); + + RecentCalls.Add(ListEmptyEntry); + } + + void OnRecentCallsListChange() + { + var handler = RecentCallsListHasChanged; + if (handler != null) + { + handler(this, new EventArgs()); + } + } + + public void RemoveEntry(CallHistoryEntry entry) + { + RecentCalls.Remove(entry); + OnRecentCallsListChange(); } /// - /// Enumeration of eCodecOccurrenceType values + /// Generic call history entry, not device specific /// - public enum eCodecOccurrenceType + public class CallHistoryEntry : CodecActiveCallItem { - /// - /// Unknown occurrence type - /// - Unknown = 0, - - /// - /// Call was placed (outgoing) - /// - Placed = 1, - - /// - /// Call was received (incoming) - /// - Received = 2, - - /// - /// Call received no answer - /// - NoAnswer = 3, + [JsonConverter(typeof(IsoDateTimeConverter))] + [JsonProperty("startTime")] + public DateTime StartTime { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("occurrenceType")] + public eCodecOccurrenceType OccurrenceType { get; set; } + [JsonProperty("occurrenceHistoryId")] + public string OccurrenceHistoryId { get; set; } } /// - /// Represents a CodecCallHistory + /// Converts a list of call history entries returned by a Cisco codec to the generic list type /// - public class CodecCallHistory + /// + /// + public void ConvertCiscoCallHistoryToGeneric(List entries) { - /// - /// Event that is raised when the recent calls list has changed - /// - public event EventHandler RecentCallsListHasChanged; + var genericEntries = new List(); - /// - /// Gets or sets the RecentCalls - /// - public List RecentCalls { get; private set; } - - /// - /// Item that gets added to the list when there are no recent calls in history - /// - CallHistoryEntry ListEmptyEntry; - - /// - /// Initializes a new instance of the CodecCallHistory class - /// - public CodecCallHistory() + foreach (CiscoCallHistory.Entry entry in entries) { - ListEmptyEntry = new CallHistoryEntry() { Name = "No Recent Calls" }; - RecentCalls = new List(); - - RecentCalls.Add(ListEmptyEntry); - } - - void OnRecentCallsListChange() - { - var handler = RecentCallsListHasChanged; - if (handler != null) + genericEntries.Add(new CallHistoryEntry() { - handler(this, new EventArgs()); - } + Name = entry.DisplayName.Value, + Number = entry.CallbackNumber.Value, + StartTime = entry.LastOccurrenceStartTime.Value, + OccurrenceHistoryId = entry.LastOccurrenceHistoryId.Value, + OccurrenceType = ConvertToOccurenceTypeEnum(entry.OccurrenceType.Value) + }); } - /// - /// RemoveEntry method - /// - public void RemoveEntry(CallHistoryEntry entry) + // Check if list is empty and if so, add an item to display No Recent Calls + if(genericEntries.Count == 0) + genericEntries.Add(ListEmptyEntry); + + RecentCalls = genericEntries; + OnRecentCallsListChange(); + } + + /// + /// Takes the Cisco occurence type and converts it to the matching enum + /// + /// + public eCodecOccurrenceType ConvertToOccurenceTypeEnum(string s) + { + switch (s) { - RecentCalls.Remove(entry); - OnRecentCallsListChange(); - } - - /// - /// Represents a CallHistoryEntry - /// - public class CallHistoryEntry : CodecActiveCallItem - { - /// - /// Gets or sets the StartTime - /// - [JsonConverter(typeof(IsoDateTimeConverter))] - [JsonProperty("startTime")] - public DateTime StartTime { get; set; } - /// - /// Gets or sets the occurrence type for this call history entry - /// - [JsonConverter(typeof(StringEnumConverter))] - [JsonProperty("occurrenceType")] - public eCodecOccurrenceType OccurrenceType { get; set; } - - /// - /// Gets or sets the occurrence history identifier - /// - [JsonProperty("occurrenceHistoryId")] - public string OccurrenceHistoryId { get; set; } - } - - /// - /// Converts a list of call history entries returned by a Cisco codec to the generic list type - /// - /// - /// - /// - /// ConvertCiscoCallHistoryToGeneric method - /// - public void ConvertCiscoCallHistoryToGeneric(List entries) - { - var genericEntries = new List(); - - foreach (CiscoCallHistory.Entry entry in entries) - { - - genericEntries.Add(new CallHistoryEntry() + case "Placed": { - Name = entry.DisplayName.Value, - Number = entry.CallbackNumber.Value, - StartTime = entry.LastOccurrenceStartTime.Value, - OccurrenceHistoryId = entry.LastOccurrenceHistoryId.Value, - OccurrenceType = ConvertToOccurenceTypeEnum(entry.OccurrenceType.Value) - }); - } - - // Check if list is empty and if so, add an item to display No Recent Calls - if (genericEntries.Count == 0) - genericEntries.Add(ListEmptyEntry); - - RecentCalls = genericEntries; - OnRecentCallsListChange(); - } - - /// - /// Takes the Cisco occurence type and converts it to the matching enum - /// - /// - /// - /// ConvertToOccurenceTypeEnum method - /// - public eCodecOccurrenceType ConvertToOccurenceTypeEnum(string s) - { - switch (s) - { - case "Placed": - { - return eCodecOccurrenceType.Placed; - } - case "Received": - { - return eCodecOccurrenceType.Received; - } - case "NoAnswer": - { - return eCodecOccurrenceType.NoAnswer; - } - default: - return eCodecOccurrenceType.Unknown; - } - + return eCodecOccurrenceType.Placed; + } + case "Received": + { + return eCodecOccurrenceType.Received; + } + case "NoAnswer": + { + return eCodecOccurrenceType.NoAnswer; + } + default: + return eCodecOccurrenceType.Unknown; } } + } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/iHasContentSharing.cs b/src/PepperDash.Essentials.Devices.Common/Codec/iHasContentSharing.cs index 2dea5b4a..e70c1db6 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/iHasContentSharing.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/iHasContentSharing.cs @@ -1,37 +1,35 @@ using PepperDash.Essentials.Core; +namespace PepperDash.Essentials.Devices.Common.Codec; -namespace PepperDash.Essentials.Devices.Common.Codec + +/// +/// Defines the contract for IHasContentSharing +/// +public interface IHasContentSharing { /// - /// Defines the contract for IHasContentSharing + /// Gets feedback indicating whether content sharing is currently active /// - public interface IHasContentSharing - { - /// - /// Gets feedback indicating whether content sharing is currently active - /// - BoolFeedback SharingContentIsOnFeedback { get; } + BoolFeedback SharingContentIsOnFeedback { get; } - /// - /// Gets feedback about the current sharing source - /// - StringFeedback SharingSourceFeedback { get; } + /// + /// Gets feedback about the current sharing source + /// + StringFeedback SharingSourceFeedback { get; } - /// - /// Gets a value indicating whether content should be automatically shared while in a call - /// - bool AutoShareContentWhileInCall { get; } + /// + /// Gets a value indicating whether content should be automatically shared while in a call + /// + bool AutoShareContentWhileInCall { get; } - /// - /// Starts content sharing - /// - void StartSharing(); + /// + /// Starts content sharing + /// + void StartSharing(); - /// - /// Stops content sharing - /// - void StopSharing(); - } - -} \ No newline at end of file + /// + /// Stops content sharing + /// + void StopSharing(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/iHasDialer.cs b/src/PepperDash.Essentials.Devices.Common/Codec/iHasDialer.cs index a5373004..f378e621 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/iHasDialer.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/iHasDialer.cs @@ -1,58 +1,57 @@ using System; -namespace PepperDash.Essentials.Devices.Common.Codec +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Requirements for a device that has dialing capabilities +/// +public interface IHasDialer { + // Add requirements for Dialer functionality + /// - /// Requirements for a device that has dialing capabilities + /// Event that is raised when call status changes /// - public interface IHasDialer - { - // Add requirements for Dialer functionality + event EventHandler CallStatusChange; - /// - /// Event that is raised when call status changes - /// - event EventHandler CallStatusChange; + /// + /// Dials the specified number + /// + /// The number to dial + void Dial(string number); - /// - /// Dials the specified number - /// - /// The number to dial - void Dial(string number); + /// + /// Ends the specified active call + /// + /// The active call to end + void EndCall(CodecActiveCallItem activeCall); - /// - /// Ends the specified active call - /// - /// The active call to end - void EndCall(CodecActiveCallItem activeCall); + /// + /// Ends all active calls + /// + void EndAllCalls(); - /// - /// Ends all active calls - /// - void EndAllCalls(); + /// + /// Accepts the specified incoming call + /// + /// The call item to accept + void AcceptCall(CodecActiveCallItem item); - /// - /// Accepts the specified incoming call - /// - /// The call item to accept - void AcceptCall(CodecActiveCallItem item); + /// + /// Rejects the specified incoming call + /// + /// The call item to reject + void RejectCall(CodecActiveCallItem item); - /// - /// Rejects the specified incoming call - /// - /// The call item to reject - void RejectCall(CodecActiveCallItem item); + /// + /// Sends DTMF digits during a call + /// + /// The DTMF digit(s) to send + void SendDtmf(string digit); - /// - /// Sends DTMF digits during a call - /// - /// The DTMF digit(s) to send - void SendDtmf(string digit); + /// + /// Gets a value indicating whether the device is currently in a call + /// + bool IsInCall { get; } +} - /// - /// Gets a value indicating whether the device is currently in a call - /// - bool IsInCall { get; } - } - -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs b/src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs index 051f3b1a..3ea3ddde 100644 --- a/src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/DSP/DspBase.cs @@ -4,153 +4,147 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials.Devices.Common.DSP +namespace PepperDash.Essentials.Devices.Common.DSP; + +/// +/// Base class for DSP devices +/// +public abstract class DspBase : EssentialsDevice, ILevelControls { /// - /// Base class for DSP devices + /// Gets the collection of level control points /// - public abstract class DspBase : EssentialsDevice, ILevelControls - { - /// - /// Gets the collection of level control points - /// - public Dictionary LevelControlPoints { get; private set; } - - /// - /// Gets the collection of dialer control points - /// - public Dictionary DialerControlPoints { get; private set; } - - /// - /// Gets the collection of switcher control points - /// - public Dictionary SwitcherControlPoints { get; private set; } - - /// - /// Initializes a new instance of the DspBase class - /// - /// The device key - /// The device name - public DspBase(string key, string name) : - base(key, name) - { - - LevelControlPoints = new Dictionary(); - DialerControlPoints = new Dictionary(); - SwitcherControlPoints = new Dictionary(); - } - - - // in audio call feedback - - // VOIP - // Phone dialer - - } - - // Fusion - // Privacy state - // Online state - // level/mutes ? - - // AC Log call stats - - // Typical presets: - // call default preset to restore levels and mutes + public Dictionary LevelControlPoints { get; private set; } /// - /// Base class for DSP control points + /// Gets the collection of dialer control points /// - public abstract class DspControlPoint : IKeyName + public Dictionary DialerControlPoints { get; private set; } + + /// + /// Gets the collection of switcher control points + /// + public Dictionary SwitcherControlPoints { get; private set; } + + /// + /// Initializes a new instance of the DspBase class + /// + /// The device key + /// The device name + public DspBase(string key, string name) : + base(key, name) { - /// - /// Gets or sets the Key - /// - public string Key { get; } - /// - /// Gets or sets the Name - /// - public string Name { get; private set; } + LevelControlPoints = new Dictionary(); + DialerControlPoints = new Dictionary(); + SwitcherControlPoints = new Dictionary(); + } - /// - /// Initializes a new instance of the DspControlPoint class - /// - /// The control point key - protected DspControlPoint(string key) => Key = key; + + // in audio call feedback + + // VOIP + // Phone dialer + +} + +// Fusion +// Privacy state +// Online state +// level/mutes ? + +// AC Log call stats + +// Typical presets: +// call default preset to restore levels and mutes + +/// +/// Base class for DSP control points +/// +public abstract class DspControlPoint : IKeyName +{ + /// + /// Gets or sets the Key + /// + public string Key { get; } + + /// + /// Gets or sets the Name + /// + public string Name { get; private set; } + + /// + /// Initializes a new instance of the DspControlPoint class + /// + /// The control point key + protected DspControlPoint(string key) => Key = key; +} + +/// +/// Base class for DSP level control points with volume and mute functionality +/// +public abstract class DspLevelControlPoint : DspControlPoint, IBasicVolumeWithFeedback +{ + /// + /// Gets or sets the MuteFeedback + /// + public BoolFeedback MuteFeedback { get; } + /// + /// Gets or sets the VolumeLevelFeedback + /// + public IntFeedback VolumeLevelFeedback { get; } + + /// + /// Initializes a new instance of the DspLevelControlPoint class + /// + /// The control point key + /// Function to get mute status + /// Function to get volume level + protected DspLevelControlPoint(string key, Func muteFeedbackFunc, Func volumeLevelFeedbackFunc) : base(key) + { + MuteFeedback = new BoolFeedback("mute", muteFeedbackFunc); + VolumeLevelFeedback = new IntFeedback("volume", volumeLevelFeedbackFunc); } /// - /// Base class for DSP level control points with volume and mute functionality + /// Turns mute off /// - public abstract class DspLevelControlPoint : DspControlPoint, IBasicVolumeWithFeedback - { - /// - /// Gets or sets the MuteFeedback - /// - public BoolFeedback MuteFeedback { get; } - /// - /// Gets or sets the VolumeLevelFeedback - /// - public IntFeedback VolumeLevelFeedback { get; } - - /// - /// Initializes a new instance of the DspLevelControlPoint class - /// - /// The control point key - /// Function to get mute status - /// Function to get volume level - protected DspLevelControlPoint(string key, Func muteFeedbackFunc, Func volumeLevelFeedbackFunc) : base(key) - { - MuteFeedback = new BoolFeedback("mute", muteFeedbackFunc); - VolumeLevelFeedback = new IntFeedback("volume", volumeLevelFeedbackFunc); - } - - /// - /// Turns mute off - /// - public abstract void MuteOff(); - /// - /// Turns mute on - /// - public abstract void MuteOn(); - /// - /// Toggles mute state - /// - public abstract void MuteToggle(); - /// - /// Sets the volume level - /// - /// The volume level to set - public abstract void SetVolume(ushort level); - /// - /// Decreases volume - /// - /// True when pressed, false when released - public abstract void VolumeDown(bool pressRelease); - /// - /// Increases volume - /// - /// True when pressed, false when released - public abstract void VolumeUp(bool pressRelease); - } - + public abstract void MuteOff(); /// - /// Base class for DSP dialer control points + /// Turns mute on /// - public abstract class DspDialerBase : DspControlPoint - { - /// - /// Initializes a new instance of the DspDialerBase class - /// - /// The dialer control point key - protected DspDialerBase(string key) : base(key) { } - } + public abstract void MuteOn(); + /// + /// Toggles mute state + /// + public abstract void MuteToggle(); + /// + /// Sets the volume level + /// + /// The volume level to set + public abstract void SetVolume(ushort level); + /// + /// Decreases volume + /// + /// True when pressed, false when released + public abstract void VolumeDown(bool pressRelease); + /// + /// Increases volume + /// + /// True when pressed, false when released + public abstract void VolumeUp(bool pressRelease); +} + +/// +/// Base class for DSP dialer control points +/// +public abstract class DspDialerBase : DspControlPoint +{ + /// + /// Initializes a new instance of the DspDialerBase class + /// + /// The dialer control point key + protected DspDialerBase(string key) : base(key) { } +} - // Main program - // VTC - // ATC - // Mics, unusual - -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs index 858cdf42..445cabcf 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs @@ -9,65 +9,66 @@ using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Displays +namespace PepperDash.Essentials.Devices.Common.Displays; + +/// +/// Represents a basic IR controlled display device. This class is intended to be used for simple displays that are controlled via IR and do not have a more robust API. It provides basic power and volume controls, as well as input switching via IR commands. +/// +public class BasicIrDisplay : DisplayBase, IBasicVolumeControls, IBridgeAdvanced { /// - /// Represents a BasicIrDisplay + /// Gets or sets the IrPort /// - public class BasicIrDisplay : DisplayBase, IBasicVolumeControls, IBridgeAdvanced + public IrOutputPortController IrPort { get; private set; } + /// + /// Gets or sets the IrPulseTime + /// + public ushort IrPulseTime { get; set; } + + /// + /// Gets the power is on feedback function + /// + protected Func PowerIsOnFeedbackFunc { - /// - /// Gets or sets the IrPort - /// - public IrOutputPortController IrPort { get; private set; } - /// - /// Gets or sets the IrPulseTime - /// - public ushort IrPulseTime { get; set; } + get { return () => _PowerIsOn; } + } - /// - /// Gets the power is on feedback function - /// - protected Func PowerIsOnFeedbackFunc - { - get { return () => _PowerIsOn; } - } - /// - /// Gets the is cooling down feedback function - /// - protected override Func IsCoolingDownFeedbackFunc - { - get { return () => _IsCoolingDown; } - } - /// - /// Gets the is warming up feedback function - /// - protected override Func IsWarmingUpFeedbackFunc - { - get { return () => _IsWarmingUp; } - } + /// + /// Gets the is cooling down feedback function + /// + protected override Func IsCoolingDownFeedbackFunc + { + get { return () => _IsCoolingDown; } + } + /// + /// Gets the is warming up feedback function + /// + protected override Func IsWarmingUpFeedbackFunc + { + get { return () => _IsWarmingUp; } + } - bool _PowerIsOn; - bool _IsWarmingUp; - bool _IsCoolingDown; + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; - /// - /// Initializes a new instance of the BasicIrDisplay class - /// - /// The device key - /// The device name - /// The IR output port - /// The path to the IR driver file - public BasicIrDisplay(string key, string name, IROutputPort port, string irDriverFilepath) - : base(key, name) - { - IrPort = new IrOutputPortController(key + "-ir", port, irDriverFilepath); - DeviceManager.AddDevice(IrPort); + /// + /// Initializes a new instance of the BasicIrDisplay class + /// + /// The device key + /// The device name + /// The IR output port + /// The path to the IR driver file + public BasicIrDisplay(string key, string name, IROutputPort port, string irDriverFilepath) + : base(key, name) + { + IrPort = new IrOutputPortController(key + "-ir", port, irDriverFilepath); + DeviceManager.AddDevice(IrPort); - IsWarmingUpFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Verbose, this, "Warming up={0}", _IsWarmingUp); - IsCoolingDownFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Verbose, this, "Cooling down={0}", _IsCoolingDown); + IsWarmingUpFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Verbose, this, "Warming up={0}", _IsWarmingUp); + IsCoolingDownFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Verbose, this, "Cooling down={0}", _IsCoolingDown); - InputPorts.AddRange(new RoutingPortCollection + InputPorts.AddRange(new RoutingPortCollection { new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, new Action(Hdmi1), this, false), @@ -84,224 +85,223 @@ namespace PepperDash.Essentials.Devices.Common.Displays new RoutingInputPort(RoutingPortNames.AntennaIn, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, new Action(Antenna), this, false), }); - } - - /// - /// Hdmi1 method - /// - public void Hdmi1() - { - IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_1, IrPulseTime); - } - - /// - /// Hdmi2 method - /// - public void Hdmi2() - { - IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_2, IrPulseTime); - } - - /// - /// Hdmi3 method - /// - public void Hdmi3() - { - IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_3, IrPulseTime); - } - - /// - /// Hdmi4 method - /// - public void Hdmi4() - { - IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_4, IrPulseTime); - } - - /// - /// Component1 method - /// - public void Component1() - { - IrPort.Pulse(IROutputStandardCommands.IROut_COMPONENT_1, IrPulseTime); - } - - /// - /// Video1 method - /// - public void Video1() - { - IrPort.Pulse(IROutputStandardCommands.IROut_VIDEO_1, IrPulseTime); - } - - /// - /// Antenna method - /// - public void Antenna() - { - IrPort.Pulse(IROutputStandardCommands.IROut_ANTENNA, IrPulseTime); - } - - #region IPower Members - - /// - /// PowerOn method - /// - /// - public override void PowerOn() - { - IrPort.Pulse(IROutputStandardCommands.IROut_POWER_ON, IrPulseTime); - _PowerIsOn = true; - } - - /// - /// PowerOff method - /// - public override void PowerOff() - { - _PowerIsOn = false; - IrPort.Pulse(IROutputStandardCommands.IROut_POWER_OFF, IrPulseTime); - } - - /// - /// PowerToggle method - /// - public override void PowerToggle() - { - _PowerIsOn = false; - IrPort.Pulse(IROutputStandardCommands.IROut_POWER, IrPulseTime); - } - - #endregion - - #region IBasicVolumeControls Members - - /// - /// VolumeUp method - /// - public void VolumeUp(bool pressRelease) - { - IrPort.PressRelease(IROutputStandardCommands.IROut_VOL_PLUS, pressRelease); - } - - /// - /// VolumeDown method - /// - public void VolumeDown(bool pressRelease) - { - IrPort.PressRelease(IROutputStandardCommands.IROut_VOL_MINUS, pressRelease); - } - - /// - /// MuteToggle method - /// - public void MuteToggle() - { - IrPort.Pulse(IROutputStandardCommands.IROut_MUTE, 200); - } - - #endregion - - void StartWarmingTimer() - { - _IsWarmingUp = true; - IsWarmingUpFeedback.FireUpdate(); - new CTimer(o => - { - _IsWarmingUp = false; - IsWarmingUpFeedback.FireUpdate(); - }, 10000); - } - - void StartCoolingTimer() - { - _IsCoolingDown = true; - IsCoolingDownFeedback.FireUpdate(); - new CTimer(o => - { - _IsCoolingDown = false; - IsCoolingDownFeedback.FireUpdate(); - }, 7000); - } - - #region IRoutingSink Members - - /// - /// Typically called by the discovery routing algorithm. - /// - /// A delegate containing the input selector method to call - /// - /// ExecuteSwitch method - /// - /// - public override void ExecuteSwitch(object inputSelector) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Switching to input '{0}'", (inputSelector as Action).ToString()); - - Action finishSwitch = () => - { - var action = inputSelector as Action; - if (action != null) - action(); - }; - - if (!_PowerIsOn) - { - PowerOn(); - EventHandler oneTimer = null; - oneTimer = (o, a) => - { - if (IsWarmingUpFeedback.BoolValue) return; // Only catch done warming - IsWarmingUpFeedback.OutputChange -= oneTimer; - finishSwitch(); - }; - IsWarmingUpFeedback.OutputChange += oneTimer; - } - else // Do it! - finishSwitch(); - } - - #endregion - - /// - /// LinkToApi method - /// - public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - LinkDisplayToApi(this, trilist, joinStart, joinMapKey, bridge); - } } /// - /// Represents a BasicIrDisplayFactory + /// Hdmi1 method /// - public class BasicIrDisplayFactory : EssentialsDeviceFactory + public void Hdmi1() { - /// - /// Initializes a new instance of the BasicIrDisplayFactory class - /// - public BasicIrDisplayFactory() - { - TypeNames = new List() { "basicirdisplay" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new BasicIrDisplay Device"); - var ir = IRPortHelper.GetIrPort(dc.Properties); - if (ir != null) - { - var display = new BasicIrDisplay(dc.Key, dc.Name, ir.Port, ir.FileName); - display.IrPulseTime = 200; // Set default pulse time for IR commands. - return display; - } - - return null; - } + IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_1, IrPulseTime); } + /// + /// Hdmi2 method + /// + public void Hdmi2() + { + IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_2, IrPulseTime); + } + + /// + /// Hdmi3 method + /// + public void Hdmi3() + { + IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_3, IrPulseTime); + } + + /// + /// Hdmi4 method + /// + public void Hdmi4() + { + IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_4, IrPulseTime); + } + + /// + /// Component1 method + /// + public void Component1() + { + IrPort.Pulse(IROutputStandardCommands.IROut_COMPONENT_1, IrPulseTime); + } + + /// + /// Video1 method + /// + public void Video1() + { + IrPort.Pulse(IROutputStandardCommands.IROut_VIDEO_1, IrPulseTime); + } + + /// + /// Antenna method + /// + public void Antenna() + { + IrPort.Pulse(IROutputStandardCommands.IROut_ANTENNA, IrPulseTime); + } + + #region IPower Members + + /// + /// PowerOn method + /// + /// + public override void PowerOn() + { + IrPort.Pulse(IROutputStandardCommands.IROut_POWER_ON, IrPulseTime); + _PowerIsOn = true; + } + + /// + /// PowerOff method + /// + public override void PowerOff() + { + _PowerIsOn = false; + IrPort.Pulse(IROutputStandardCommands.IROut_POWER_OFF, IrPulseTime); + } + + /// + /// PowerToggle method + /// + public override void PowerToggle() + { + _PowerIsOn = false; + IrPort.Pulse(IROutputStandardCommands.IROut_POWER, IrPulseTime); + } + + #endregion + + #region IBasicVolumeControls Members + + /// + /// VolumeUp method + /// + public void VolumeUp(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_VOL_PLUS, pressRelease); + } + + /// + /// VolumeDown method + /// + public void VolumeDown(bool pressRelease) + { + IrPort.PressRelease(IROutputStandardCommands.IROut_VOL_MINUS, pressRelease); + } + + /// + /// MuteToggle method + /// + public void MuteToggle() + { + IrPort.Pulse(IROutputStandardCommands.IROut_MUTE, 200); + } + + #endregion + + void StartWarmingTimer() + { + _IsWarmingUp = true; + IsWarmingUpFeedback.FireUpdate(); + new CTimer(o => + { + _IsWarmingUp = false; + IsWarmingUpFeedback.FireUpdate(); + }, 10000); + } + + void StartCoolingTimer() + { + _IsCoolingDown = true; + IsCoolingDownFeedback.FireUpdate(); + new CTimer(o => + { + _IsCoolingDown = false; + IsCoolingDownFeedback.FireUpdate(); + }, 7000); + } + + #region IRoutingSink Members + + /// + /// Typically called by the discovery routing algorithm. + /// + /// A delegate containing the input selector method to call + /// + /// ExecuteSwitch method + /// + /// + public override void ExecuteSwitch(object inputSelector) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Switching to input '{0}'", (inputSelector as Action).ToString()); + + Action finishSwitch = () => + { + var action = inputSelector as Action; + if (action != null) + action(); + }; + + if (!_PowerIsOn) + { + PowerOn(); + EventHandler oneTimer = null; + oneTimer = (o, a) => + { + if (IsWarmingUpFeedback.BoolValue) return; // Only catch done warming + IsWarmingUpFeedback.OutputChange -= oneTimer; + finishSwitch(); + }; + IsWarmingUpFeedback.OutputChange += oneTimer; + } + else // Do it! + finishSwitch(); + } + + #endregion + + /// + /// LinkToApi method + /// + public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + LinkDisplayToApi(this, trilist, joinStart, joinMapKey, bridge); + } +} + +/// +/// Factory for creating BasicIrDisplay devices +/// +public class BasicIrDisplayFactory : EssentialsDeviceFactory +{ + /// + /// Initializes a new instance of the BasicIrDisplayFactory class + /// + public BasicIrDisplayFactory() + { + TypeNames = new List() { "basicirdisplay" }; + } + + /// + /// BuildDevice method + /// + /// + /// + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new BasicIrDisplay Device"); + var ir = IRPortHelper.GetIrPort(dc.Properties); + if (ir != null) + { + var display = new BasicIrDisplay(dc.Key, dc.Name, ir.Port, ir.FileName); + display.IrPulseTime = 200; // Set default pulse time for IR commands. + return display; + } + + return null; + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs index 5db03a30..fba36564 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs @@ -13,426 +13,425 @@ using PepperDash.Essentials.Core.Routing; using Serilog.Events; using Feedback = PepperDash.Essentials.Core.Feedback; -namespace PepperDash.Essentials.Devices.Common.Displays +namespace PepperDash.Essentials.Devices.Common.Displays; + +/// +/// Abstract base class for display devices that provides common display functionality +/// including power control, input switching, and routing capabilities. +/// +public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, IHasFeedback { + private RoutingInputPort _currentInputPort; + /// - /// Abstract base class for display devices that provides common display functionality - /// including power control, input switching, and routing capabilities. + /// Gets or sets the current input port that is selected on the display. /// - public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, IHasFeedback + public RoutingInputPort CurrentInputPort { - private RoutingInputPort _currentInputPort; - - /// - /// Gets or sets the current input port that is selected on the display. - /// - public RoutingInputPort CurrentInputPort + get { - get - { - return _currentInputPort; - } - - protected set - { - if (_currentInputPort == value) return; - - _currentInputPort = value; - - InputChanged?.Invoke(this, _currentInputPort); - } + return _currentInputPort; } - /// - /// Event that is raised when the input changes on the display. - /// - public event InputChangedEventHandler InputChanged; - - /// - /// Event that is raised when the current source information changes. - /// - public event SourceInfoChangeHandler CurrentSourceChange; - - /// - /// Gets or sets the CurrentSourceInfoKey - /// - public string CurrentSourceInfoKey { get; set; } - - /// - /// Gets or sets the current source information for the display. - /// - public SourceListItem CurrentSourceInfo + protected set { - get - { - return _CurrentSourceInfo; - } - set - { - if (value == _CurrentSourceInfo) return; + if (_currentInputPort == value) return; - var handler = CurrentSourceChange; + _currentInputPort = value; - handler?.Invoke(_CurrentSourceInfo, ChangeType.WillChange); - - _CurrentSourceInfo = value; - - handler?.Invoke(_CurrentSourceInfo, ChangeType.DidChange); - } + InputChanged?.Invoke(this, _currentInputPort); } - SourceListItem _CurrentSourceInfo; + } - /// - public Dictionary CurrentSources { get; private set; } + /// + /// Event that is raised when the input changes on the display. + /// + public event InputChangedEventHandler InputChanged; - /// - public Dictionary CurrentSourceKeys { get; private set; } + /// + /// Event that is raised when the current source information changes. + /// + public event SourceInfoChangeHandler CurrentSourceChange; - /// - public event EventHandler CurrentSourcesChanged; + /// + /// Gets or sets the CurrentSourceInfoKey + /// + public string CurrentSourceInfoKey { get; set; } - /// - /// Gets feedback indicating whether the display is currently cooling down after being powered off. - /// - public BoolFeedback IsCoolingDownFeedback { get; protected set; } - - /// - /// Gets or sets the IsWarmingUpFeedback - /// - public BoolFeedback IsWarmingUpFeedback { get; private set; } - - /// - /// Gets or sets the UsageTracker - /// - public UsageTracking UsageTracker { get; set; } - - /// - /// Gets or sets the WarmupTime - /// - public uint WarmupTime { get; set; } - - /// - /// Gets or sets the CooldownTime - /// - public uint CooldownTime { get; set; } - - /// - /// Abstract function that must be implemented by derived classes to provide the cooling down feedback value. - /// Must be implemented by concrete sub-classes. - /// - abstract protected Func IsCoolingDownFeedbackFunc { get; } - - /// - /// Abstract function that must be implemented by derived classes to provide the warming up feedback value. - /// Must be implemented by concrete sub-classes. - /// - abstract protected Func IsWarmingUpFeedbackFunc { get; } - - /// - /// Timer used for managing display warmup timing. - /// - protected CTimer WarmupTimer; - - /// - /// Timer used for managing display cooldown timing. - /// - protected CTimer CooldownTimer; - - #region IRoutingInputs Members - - /// - /// Gets the collection of input ports available on this display device. - /// - public RoutingPortCollection InputPorts { get; private set; } - - #endregion - - /// - /// Initializes a new instance of the DisplayBase class. - /// - /// The unique key identifier for this display device. - /// The friendly name for this display device. - protected DisplayBase(string key, string name) - : base(key, name) + /// + /// Gets or sets the current source information for the display. + /// + public SourceListItem CurrentSourceInfo + { + get { - IsCoolingDownFeedback = new BoolFeedback("IsCoolingDown", IsCoolingDownFeedbackFunc); - IsWarmingUpFeedback = new BoolFeedback("IsWarmingUp", IsWarmingUpFeedbackFunc); + return _CurrentSourceInfo; + } + set + { + if (value == _CurrentSourceInfo) return; - Feedbacks.Add(IsCoolingDownFeedback); - Feedbacks.Add(IsWarmingUpFeedback); + var handler = CurrentSourceChange; - InputPorts = new RoutingPortCollection(); + handler?.Invoke(_CurrentSourceInfo, ChangeType.WillChange); - CurrentSources = new Dictionary + _CurrentSourceInfo = value; + + handler?.Invoke(_CurrentSourceInfo, ChangeType.DidChange); + } + } + SourceListItem _CurrentSourceInfo; + + /// + public Dictionary CurrentSources { get; private set; } + + /// + public Dictionary CurrentSourceKeys { get; private set; } + + /// + public event EventHandler CurrentSourcesChanged; + + /// + /// Gets feedback indicating whether the display is currently cooling down after being powered off. + /// + public BoolFeedback IsCoolingDownFeedback { get; protected set; } + + /// + /// Gets or sets the IsWarmingUpFeedback + /// + public BoolFeedback IsWarmingUpFeedback { get; private set; } + + /// + /// Gets or sets the UsageTracker + /// + public UsageTracking UsageTracker { get; set; } + + /// + /// Gets or sets the WarmupTime + /// + public uint WarmupTime { get; set; } + + /// + /// Gets or sets the CooldownTime + /// + public uint CooldownTime { get; set; } + + /// + /// Abstract function that must be implemented by derived classes to provide the cooling down feedback value. + /// Must be implemented by concrete sub-classes. + /// + abstract protected Func IsCoolingDownFeedbackFunc { get; } + + /// + /// Abstract function that must be implemented by derived classes to provide the warming up feedback value. + /// Must be implemented by concrete sub-classes. + /// + abstract protected Func IsWarmingUpFeedbackFunc { get; } + + /// + /// Timer used for managing display warmup timing. + /// + protected CTimer WarmupTimer; + + /// + /// Timer used for managing display cooldown timing. + /// + protected CTimer CooldownTimer; + + #region IRoutingInputs Members + + /// + /// Gets the collection of input ports available on this display device. + /// + public RoutingPortCollection InputPorts { get; private set; } + + #endregion + + /// + /// Initializes a new instance of the DisplayBase class. + /// + /// The unique key identifier for this display device. + /// The friendly name for this display device. + protected DisplayBase(string key, string name) + : base(key, name) + { + IsCoolingDownFeedback = new BoolFeedback("IsCoolingDown", IsCoolingDownFeedbackFunc); + IsWarmingUpFeedback = new BoolFeedback("IsWarmingUp", IsWarmingUpFeedbackFunc); + + Feedbacks.Add(IsCoolingDownFeedback); + Feedbacks.Add(IsWarmingUpFeedback); + + InputPorts = new RoutingPortCollection(); + + CurrentSources = new Dictionary { { eRoutingSignalType.Audio, null }, { eRoutingSignalType.Video, null }, }; - CurrentSourceKeys = new Dictionary + CurrentSourceKeys = new Dictionary { { eRoutingSignalType.Audio, string.Empty }, { eRoutingSignalType.Video, string.Empty }, }; + } + + /// + /// Powers on the display device. Must be implemented by derived classes. + /// + public abstract void PowerOn(); + + /// + /// Powers off the display device. Must be implemented by derived classes. + /// + public abstract void PowerOff(); + + /// + /// Toggles the power state of the display device. Must be implemented by derived classes. + /// + public abstract void PowerToggle(); + + /// + /// Gets the collection of feedback objects for this display device. + /// + /// + public virtual FeedbackCollection Feedbacks { get; private set; } = new FeedbackCollection(); + + + /// + /// Executes a switch to the specified input on the display device. Must be implemented by derived classes. + /// + /// The selector object that identifies which input to switch to. + public abstract void ExecuteSwitch(object selector); + + /// + /// Links the display device to an API using a trilist, join start, join map key, and bridge. + /// This overload uses serialized join map configuration. + /// + /// The display device to link. + /// The BasicTriList for communication. + /// The starting join number for the device. + /// The key for the join map configuration. + /// The EISC API bridge instance. + protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey, + EiscApiAdvanced bridge) + { + var joinMap = new DisplayControllerJoinMap(joinStart); + + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + + if (bridge != null) + { + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); } - /// - /// Powers on the display device. Must be implemented by derived classes. - /// - public abstract void PowerOn(); + LinkDisplayToApi(displayDevice, trilist, joinMap); + } - /// - /// Powers off the display device. Must be implemented by derived classes. - /// - public abstract void PowerOff(); + /// + /// Links the display device to an API using a trilist and join map. + /// This overload uses a pre-configured join map instance. + /// + /// The display device to link. + /// The BasicTriList for communication. + /// The join map configuration for the device. + protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, DisplayControllerJoinMap joinMap) + { + this.LogDebug("Linking to Trilist {ipId}", trilist.ID.ToString("X")); + this.LogDebug("Linking to Display: {displayName}", displayDevice.Name); - /// - /// Toggles the power state of the display device. Must be implemented by derived classes. - /// - public abstract void PowerToggle(); + trilist.StringInput[joinMap.Name.JoinNumber].StringValue = displayDevice.Name; - /// - /// Gets the collection of feedback objects for this display device. - /// - /// - public virtual FeedbackCollection Feedbacks { get; private set; } = new FeedbackCollection(); - - - /// - /// Executes a switch to the specified input on the display device. Must be implemented by derived classes. - /// - /// The selector object that identifies which input to switch to. - public abstract void ExecuteSwitch(object selector); - - /// - /// Links the display device to an API using a trilist, join start, join map key, and bridge. - /// This overload uses serialized join map configuration. - /// - /// The display device to link. - /// The BasicTriList for communication. - /// The starting join number for the device. - /// The key for the join map configuration. - /// The EISC API bridge instance. - protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey, - EiscApiAdvanced bridge) + if (displayDevice is ICommunicationMonitor commMonitor) { - var joinMap = new DisplayControllerJoinMap(joinStart); - - var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); - - if (!string.IsNullOrEmpty(joinMapSerialized)) - joinMap = JsonConvert.DeserializeObject(joinMapSerialized); - - if (bridge != null) - { - bridge.AddJoinMap(Key, joinMap); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); - } - - LinkDisplayToApi(displayDevice, trilist, joinMap); + commMonitor.CommunicationMonitor.IsOnlineFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); } - /// - /// Links the display device to an API using a trilist and join map. - /// This overload uses a pre-configured join map instance. - /// - /// The display device to link. - /// The BasicTriList for communication. - /// The join map configuration for the device. - protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, DisplayControllerJoinMap joinMap) + // TODO: revisit this as there could be issues with this approach + var inputNumber = 0; + var inputKeys = new List(); + + var inputNumberFeedback = new IntFeedback("inputNumber", () => inputNumber); + + // Add input number feedback to the device feedback collection to keep it around... + Feedbacks.Add(inputNumberFeedback); + + // Two way feedbacks + if (displayDevice is TwoWayDisplayBase twoWayDisplay) { - this.LogDebug("Linking to Trilist {ipId}", trilist.ID.ToString("X")); - this.LogDebug("Linking to Display: {displayName}", displayDevice.Name); + trilist.SetBool(joinMap.IsTwoWayDisplay.JoinNumber, true); - trilist.StringInput[joinMap.Name.JoinNumber].StringValue = displayDevice.Name; + twoWayDisplay.CurrentInputFeedback.OutputChange += (o, a) => this.LogDebug("CurrentInputFeedback_OutputChange {input}", a.StringValue); - if (displayDevice is ICommunicationMonitor commMonitor) + inputNumberFeedback.LinkInputSig(trilist.UShortInput[joinMap.InputSelect.JoinNumber]); + + twoWayDisplay.PowerIsOnFeedback.OutputChange += (o, a) => { - commMonitor.CommunicationMonitor.IsOnlineFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); - } - - // TODO: revisit this as there could be issues with this approach - var inputNumber = 0; - var inputKeys = new List(); - - var inputNumberFeedback = new IntFeedback("inputNumber", () => inputNumber); - - // Add input number feedback to the device feedback collection to keep it around... - Feedbacks.Add(inputNumberFeedback); - - // Two way feedbacks - if (displayDevice is TwoWayDisplayBase twoWayDisplay) - { - trilist.SetBool(joinMap.IsTwoWayDisplay.JoinNumber, true); - - twoWayDisplay.CurrentInputFeedback.OutputChange += (o, a) => this.LogDebug("CurrentInputFeedback_OutputChange {input}", a.StringValue); - - inputNumberFeedback.LinkInputSig(trilist.UShortInput[joinMap.InputSelect.JoinNumber]); - - twoWayDisplay.PowerIsOnFeedback.OutputChange += (o, a) => + if (!a.BoolValue) { - if (!a.BoolValue) - { - inputNumber = 102; - inputNumberFeedback.FireUpdate(); + inputNumber = 102; + inputNumberFeedback.FireUpdate(); - } - else - { - inputNumber = 0; - inputNumberFeedback.FireUpdate(); - } - }; - - twoWayDisplay.PowerIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.PowerOff.JoinNumber]); - twoWayDisplay.PowerIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PowerOn.JoinNumber]); - } - - // Power Off - trilist.SetSigTrueAction(joinMap.PowerOff.JoinNumber, () => - { - inputNumber = 102; - inputNumberFeedback.FireUpdate(); - displayDevice.PowerOff(); - }); - - // PowerOn - trilist.SetSigTrueAction(joinMap.PowerOn.JoinNumber, () => - { - inputNumber = 0; - inputNumberFeedback.FireUpdate(); - displayDevice.PowerOn(); - }); - - - for (int i = 0; i < displayDevice.InputPorts.Count; i++) - { - var localindex = i; - if (localindex >= joinMap.InputNamesOffset.JoinSpan) - { - this.LogWarning("Device has {inputCount} inputs. The Join Map allows up to {joinSpan} inputs. Discarding inputs {discardStart} - {discardEnd} from bridge.", - displayDevice.InputPorts.Count, joinMap.InputNamesOffset.JoinSpan, localindex + 1, displayDevice.InputPorts.Count); - - continue; } else { - inputKeys.Add(displayDevice.InputPorts[localindex].Key); - - var tempKey = inputKeys.ElementAt(localindex); - - trilist.SetSigTrueAction((ushort)(joinMap.InputSelectOffset.JoinNumber + localindex), () => displayDevice.ExecuteSwitch(displayDevice.InputPorts[tempKey].Selector)); - - this.LogDebug("Setting Input Select Action on Digital Join {joinNumber} to Input: {input}", joinMap.InputSelectOffset.JoinNumber + localindex, displayDevice.InputPorts[tempKey].Key); - - trilist.SetString((uint)(joinMap.InputNamesOffset.JoinNumber + localindex), displayDevice.InputPorts[localindex].Key); - } - } - - this.LogDebug("Setting Input Select Action on Analog Join {inputSelectJoin}", joinMap.InputSelect); - - trilist.SetUShortSigAction(joinMap.InputSelect.JoinNumber, (requestedInput) => - { - if (requestedInput == 0) - { - displayDevice.PowerOff(); inputNumber = 0; - return; + inputNumberFeedback.FireUpdate(); } + }; - // using 1-based indexing for inputs coming from SIMPL, so need to check if the input is <= the count, not < - if (requestedInput > 0 && requestedInput <= displayDevice.InputPorts.Count && requestedInput != inputNumber) - { - displayDevice.ExecuteSwitch(displayDevice.InputPorts.ElementAt(requestedInput - 1).Selector); - - inputNumber = requestedInput; - - return; - } - - if (requestedInput == 102) - { - displayDevice.PowerToggle(); - return; - } - - if (displayDevice is TwoWayDisplayBase) - { - inputNumberFeedback?.FireUpdate(); - } - }); - - - var volumeDisplay = displayDevice as IBasicVolumeControls; - if (volumeDisplay == null) return; - - trilist.SetBoolSigAction(joinMap.VolumeUp.JoinNumber, volumeDisplay.VolumeUp); - trilist.SetBoolSigAction(joinMap.VolumeDown.JoinNumber, volumeDisplay.VolumeDown); - trilist.SetSigTrueAction(joinMap.VolumeMute.JoinNumber, volumeDisplay.MuteToggle); - - var volumeDisplayWithFeedback = volumeDisplay as IBasicVolumeWithFeedback; - - if (volumeDisplayWithFeedback == null) return; - trilist.SetSigTrueAction(joinMap.VolumeMuteOn.JoinNumber, volumeDisplayWithFeedback.MuteOn); - trilist.SetSigTrueAction(joinMap.VolumeMuteOff.JoinNumber, volumeDisplayWithFeedback.MuteOff); - - - trilist.SetUShortSigAction(joinMap.VolumeLevel.JoinNumber, volumeDisplayWithFeedback.SetVolume); - volumeDisplayWithFeedback.VolumeLevelFeedback.LinkInputSig(trilist.UShortInput[joinMap.VolumeLevel.JoinNumber]); - volumeDisplayWithFeedback.MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMute.JoinNumber]); - volumeDisplayWithFeedback.MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMuteOn.JoinNumber]); - volumeDisplayWithFeedback.MuteFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.VolumeMuteOff.JoinNumber]); + twoWayDisplay.PowerIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.PowerOff.JoinNumber]); + twoWayDisplay.PowerIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PowerOn.JoinNumber]); } - /// - public virtual void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + // Power Off + trilist.SetSigTrueAction(joinMap.PowerOff.JoinNumber, () => { - foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) - { - var flagValue = Convert.ToInt32(type); - // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). - // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. - if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) - { - this.LogDebug("Skipping {type}", type); - continue; - } + inputNumber = 102; + inputNumberFeedback.FireUpdate(); + displayDevice.PowerOff(); + }); - this.LogDebug("setting {type}", type); - - if (signalType.HasFlag(type)) - { - UpdateCurrentSources(type, sourceListKey, sourceListItem); - } - } - // Raise the CurrentSourcesChanged event - CurrentSourcesChanged?.Invoke(this, EventArgs.Empty); - } - - private void UpdateCurrentSources(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + // PowerOn + trilist.SetSigTrueAction(joinMap.PowerOn.JoinNumber, () => { - if (CurrentSources.ContainsKey(signalType)) + inputNumber = 0; + inputNumberFeedback.FireUpdate(); + displayDevice.PowerOn(); + }); + + + for (int i = 0; i < displayDevice.InputPorts.Count; i++) + { + var localindex = i; + if (localindex >= joinMap.InputNamesOffset.JoinSpan) { - CurrentSources[signalType] = sourceListItem; + this.LogWarning("Device has {inputCount} inputs. The Join Map allows up to {joinSpan} inputs. Discarding inputs {discardStart} - {discardEnd} from bridge.", + displayDevice.InputPorts.Count, joinMap.InputNamesOffset.JoinSpan, localindex + 1, displayDevice.InputPorts.Count); + + continue; } else { - CurrentSources.Add(signalType, sourceListItem); - } + inputKeys.Add(displayDevice.InputPorts[localindex].Key); - // Update the current source key for the specified signal type - if (CurrentSourceKeys.ContainsKey(signalType)) - { - CurrentSourceKeys[signalType] = sourceListKey; - } - else - { - CurrentSourceKeys.Add(signalType, sourceListKey); + var tempKey = inputKeys.ElementAt(localindex); + + trilist.SetSigTrueAction((ushort)(joinMap.InputSelectOffset.JoinNumber + localindex), () => displayDevice.ExecuteSwitch(displayDevice.InputPorts[tempKey].Selector)); + + this.LogDebug("Setting Input Select Action on Digital Join {joinNumber} to Input: {input}", joinMap.InputSelectOffset.JoinNumber + localindex, displayDevice.InputPorts[tempKey].Key); + + trilist.SetString((uint)(joinMap.InputNamesOffset.JoinNumber + localindex), displayDevice.InputPorts[localindex].Key); } } + this.LogDebug("Setting Input Select Action on Analog Join {inputSelectJoin}", joinMap.InputSelect); + + trilist.SetUShortSigAction(joinMap.InputSelect.JoinNumber, (requestedInput) => + { + if (requestedInput == 0) + { + displayDevice.PowerOff(); + inputNumber = 0; + return; + } + + // using 1-based indexing for inputs coming from SIMPL, so need to check if the input is <= the count, not < + if (requestedInput > 0 && requestedInput <= displayDevice.InputPorts.Count && requestedInput != inputNumber) + { + displayDevice.ExecuteSwitch(displayDevice.InputPorts.ElementAt(requestedInput - 1).Selector); + + inputNumber = requestedInput; + + return; + } + + if (requestedInput == 102) + { + displayDevice.PowerToggle(); + return; + } + + if (displayDevice is TwoWayDisplayBase) + { + inputNumberFeedback?.FireUpdate(); + } + }); + + + var volumeDisplay = displayDevice as IBasicVolumeControls; + if (volumeDisplay == null) return; + + trilist.SetBoolSigAction(joinMap.VolumeUp.JoinNumber, volumeDisplay.VolumeUp); + trilist.SetBoolSigAction(joinMap.VolumeDown.JoinNumber, volumeDisplay.VolumeDown); + trilist.SetSigTrueAction(joinMap.VolumeMute.JoinNumber, volumeDisplay.MuteToggle); + + var volumeDisplayWithFeedback = volumeDisplay as IBasicVolumeWithFeedback; + + if (volumeDisplayWithFeedback == null) return; + trilist.SetSigTrueAction(joinMap.VolumeMuteOn.JoinNumber, volumeDisplayWithFeedback.MuteOn); + trilist.SetSigTrueAction(joinMap.VolumeMuteOff.JoinNumber, volumeDisplayWithFeedback.MuteOff); + + + trilist.SetUShortSigAction(joinMap.VolumeLevel.JoinNumber, volumeDisplayWithFeedback.SetVolume); + volumeDisplayWithFeedback.VolumeLevelFeedback.LinkInputSig(trilist.UShortInput[joinMap.VolumeLevel.JoinNumber]); + volumeDisplayWithFeedback.MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMute.JoinNumber]); + volumeDisplayWithFeedback.MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMuteOn.JoinNumber]); + volumeDisplayWithFeedback.MuteFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.VolumeMuteOff.JoinNumber]); } -} \ No newline at end of file + + /// + public virtual void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + { + foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) + { + var flagValue = Convert.ToInt32(type); + // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). + // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. + if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) + { + this.LogDebug("Skipping {type}", type); + continue; + } + + this.LogDebug("setting {type}", type); + + if (signalType.HasFlag(type)) + { + UpdateCurrentSources(type, sourceListKey, sourceListItem); + } + } + // Raise the CurrentSourcesChanged event + CurrentSourcesChanged?.Invoke(this, EventArgs.Empty); + } + + private void UpdateCurrentSources(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + { + if (CurrentSources.ContainsKey(signalType)) + { + CurrentSources[signalType] = sourceListItem; + } + else + { + CurrentSources.Add(signalType, sourceListItem); + } + + // Update the current source key for the specified signal type + if (CurrentSourceKeys.ContainsKey(signalType)) + { + CurrentSourceKeys[signalType] = sourceListKey; + } + else + { + CurrentSourceKeys.Add(signalType, sourceListKey); + } + } + +} diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/InputInterfaces.cs b/src/PepperDash.Essentials.Devices.Common/Displays/InputInterfaces.cs index b4314c6b..4d04fd28 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/InputInterfaces.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/InputInterfaces.cs @@ -1,89 +1,87 @@ using System; -namespace PepperDash.Essentials.Devices.Displays +namespace PepperDash.Essentials.Devices.Displays; + +/// +/// Defines the contract for IInputHdmi1 +/// +[Obsolete()] +public interface IInputHdmi1 { /// - /// Defines the contract for IInputHdmi1 + /// Switches to HDMI 1 input /// - [Obsolete()] - public interface IInputHdmi1 - { - /// - /// Switches to HDMI 1 input - /// - void InputHdmi1(); - } + void InputHdmi1(); +} +/// +/// Defines the contract for IInputHdmi2 +/// +[Obsolete()] +public interface IInputHdmi2 +{ /// - /// Defines the contract for IInputHdmi2 + /// Switches to HDMI 2 input /// - [Obsolete()] - public interface IInputHdmi2 - { - /// - /// Switches to HDMI 2 input - /// - void InputHdmi2(); - } + void InputHdmi2(); +} +/// +/// Defines the contract for IInputHdmi3 +/// +[Obsolete()] +public interface IInputHdmi3 +{ /// - /// Defines the contract for IInputHdmi3 + /// Switches to HDMI 3 input /// - [Obsolete()] - public interface IInputHdmi3 - { - /// - /// Switches to HDMI 3 input - /// - void InputHdmi3(); - } + void InputHdmi3(); +} +/// +/// Defines the contract for IInputHdmi4 +/// +[Obsolete()] +public interface IInputHdmi4 +{ /// - /// Defines the contract for IInputHdmi4 + /// Switches to HDMI 4 input /// - [Obsolete()] - public interface IInputHdmi4 - { - /// - /// Switches to HDMI 4 input - /// - void InputHdmi4(); - } + void InputHdmi4(); +} +/// +/// Defines the contract for IInputDisplayPort1 +/// +[Obsolete()] +public interface IInputDisplayPort1 +{ /// - /// Defines the contract for IInputDisplayPort1 + /// Switches to DisplayPort 1 input /// - [Obsolete()] - public interface IInputDisplayPort1 - { - /// - /// Switches to DisplayPort 1 input - /// - void InputDisplayPort1(); - } + void InputDisplayPort1(); +} +/// +/// Defines the contract for IInputDisplayPort2 +/// +[Obsolete()] +public interface IInputDisplayPort2 +{ /// - /// Defines the contract for IInputDisplayPort2 + /// Switches to DisplayPort 2 input /// - [Obsolete()] - public interface IInputDisplayPort2 - { - /// - /// Switches to DisplayPort 2 input - /// - void InputDisplayPort2(); - } + void InputDisplayPort2(); +} +/// +/// Defines the contract for IInputVga1 +/// +[Obsolete()] +public interface IInputVga1 +{ /// - /// Defines the contract for IInputVga1 + /// Switches to VGA 1 input /// - [Obsolete()] - public interface IInputVga1 - { - /// - /// Switches to VGA 1 input - /// - void InputVga1(); - } - -} \ No newline at end of file + void InputVga1(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs index 6f46b1a4..ead4c0eb 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs @@ -10,82 +10,62 @@ using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Displays +namespace PepperDash.Essentials.Devices.Common.Displays; + +/// +/// Represents a mock display device for testing and simulation purposes. +/// +public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs, IRoutingSinkWithSwitchingWithInputPort, IHasPowerControlWithFeedback { - /// - /// Represents a MockDisplay - /// - public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs, IRoutingSinkWithSwitchingWithInputPort, IHasPowerControlWithFeedback + public ISelectableItems Inputs { get; private set; } + + bool _PowerIsOn; + bool _IsWarmingUp; + bool _IsCoolingDown; + + protected override Func PowerIsOnFeedbackFunc { - /// - /// Gets or sets the Inputs - /// - public ISelectableItems Inputs { get; private set; } - - bool _PowerIsOn; - bool _IsWarmingUp; - bool _IsCoolingDown; - - /// - /// Gets the power is on feedback function - /// - protected override Func PowerIsOnFeedbackFunc + get { - get - { - return () => - { - return _PowerIsOn; - }; - } - } - /// - /// Gets the is cooling down feedback function - /// - protected override Func IsCoolingDownFeedbackFunc - { - get - { - return () => + return () => { - return _IsCoolingDown; + return _PowerIsOn; }; - } } - /// - /// Gets the is warming up feedback function - /// - protected override Func IsWarmingUpFeedbackFunc + } + protected override Func IsCoolingDownFeedbackFunc + { + get { - get + return () => { - return () => - { - return _IsWarmingUp; - }; - } + return _IsCoolingDown; + }; } - /// - /// Gets the current input feedback function - /// - protected override Func CurrentInputFeedbackFunc { get { return () => Inputs.CurrentItem; } } - - int VolumeHeldRepeatInterval = 200; - ushort VolumeInterval = 655; - ushort _FakeVolumeLevel = 31768; - bool _IsMuted; - - /// - /// Initializes a new instance of the MockDisplay class - /// - /// The device key - /// The device name - public MockDisplay(string key, string name) - : base(key, name) + } + protected override Func IsWarmingUpFeedbackFunc + { + get { - Inputs = new MockDisplayInputs + return () => { - Items = new Dictionary + return _IsWarmingUp; + }; + } + } + protected override Func CurrentInputFeedbackFunc { get { return () => Inputs.CurrentItem; } } + + int VolumeHeldRepeatInterval = 200; + ushort VolumeInterval = 655; + ushort _FakeVolumeLevel = 31768; + bool _IsMuted; + + public MockDisplay(string key, string name) + : base(key, name) + { + Inputs = new MockDisplayInputs + { + Items = new Dictionary { { "HDMI1", new MockDisplayInput ( "HDMI1", "HDMI 1",this ) }, { "HDMI2", new MockDisplayInput ("HDMI2", "HDMI 2",this ) }, @@ -93,276 +73,239 @@ namespace PepperDash.Essentials.Devices.Common.Displays { "HDMI4", new MockDisplayInput ("HDMI4", "HDMI 4",this )}, { "DP", new MockDisplayInput ("DP", "DisplayPort", this ) } } - }; + }; - Inputs.CurrentItemChanged += (o, a) => CurrentInputFeedback.FireUpdate(); + Inputs.CurrentItemChanged += (o, a) => CurrentInputFeedback.FireUpdate(); - var hdmiIn1 = new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, + var hdmiIn1 = new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, "HDMI1", this); - var hdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, - eRoutingPortConnectionType.Hdmi, "HDMI2", this); - var hdmiIn3 = new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, - eRoutingPortConnectionType.Hdmi, "HDMI3", this); - var hdmiIn4 = new RoutingInputPort(RoutingPortNames.HdmiIn4, eRoutingSignalType.AudioVideo, - eRoutingPortConnectionType.Hdmi, "HDMI4", this); - var dpIn = new RoutingInputPort(RoutingPortNames.DisplayPortIn, eRoutingSignalType.AudioVideo, - eRoutingPortConnectionType.DisplayPort, "DP", this); - InputPorts.AddRange(new[] { hdmiIn1, hdmiIn2, hdmiIn3, hdmiIn4, dpIn }); - - VolumeLevelFeedback = new IntFeedback("volume", () => { return _FakeVolumeLevel; }); - MuteFeedback = new BoolFeedback("muteOn", () => _IsMuted); - - WarmupTime = 10000; - CooldownTime = 10000; - } - - /// - /// PowerOn method - /// - /// - public override void PowerOn() - { - if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) - { - _IsWarmingUp = true; - IsWarmingUpFeedback.InvokeFireUpdate(); - // Fake power-up cycle - WarmupTimer = new CTimer(o => - { - _IsWarmingUp = false; - _PowerIsOn = true; - IsWarmingUpFeedback.InvokeFireUpdate(); - PowerIsOnFeedback.InvokeFireUpdate(); - }, WarmupTime); - } - } - - /// - /// PowerOff method - /// - /// - public override void PowerOff() - { - // If a display has unreliable-power off feedback, just override this and - // remove this check. - if (PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) - { - _IsCoolingDown = true; - IsCoolingDownFeedback.InvokeFireUpdate(); - // Fake cool-down cycle - CooldownTimer = new CTimer(o => - { - Debug.LogMessage(LogEventLevel.Verbose, "Cooldown timer ending", this); - _IsCoolingDown = false; - IsCoolingDownFeedback.InvokeFireUpdate(); - _PowerIsOn = false; - PowerIsOnFeedback.InvokeFireUpdate(); - }, CooldownTime); - } - } - - /// - /// PowerToggle method - /// - /// - public override void PowerToggle() - { - if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) - PowerOff(); - else if (!PowerIsOnFeedback.BoolValue && !IsCoolingDownFeedback.BoolValue) - PowerOn(); - } - - /// - /// ExecuteSwitch method - /// - /// - public override void ExecuteSwitch(object selector) - { - try - { - Debug.LogMessage(LogEventLevel.Verbose, "ExecuteSwitch: {0}", this, selector); - - if (!_PowerIsOn) - { - PowerOn(); - } - - if (!Inputs.Items.TryGetValue(selector.ToString(), out var input)) - return; - - Debug.LogMessage(LogEventLevel.Verbose, "Selected input: {input}", this, input.Key); - input.Select(); - - var inputPort = InputPorts.FirstOrDefault(port => - { - Debug.LogMessage(LogEventLevel.Verbose, "Checking input port {inputPort} with selector {portSelector} against {selector}", this, port, port.Selector, selector); - return port.Selector.ToString() == selector.ToString(); - }); - - if (inputPort == null) - { - Debug.LogMessage(LogEventLevel.Verbose, "Unable to find input port for selector {selector}", this, selector); - return; - } - - Debug.LogMessage(LogEventLevel.Verbose, "Setting current input port to {inputPort}", this, inputPort); - CurrentInputPort = inputPort; - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error making switch: {Exception}", this, ex); - } - } - - /// - /// SetInput method - /// - public void SetInput(string selector) - { - ISelectableItem currentInput = null; - - try - { - currentInput = Inputs.Items.SingleOrDefault(Inputs => Inputs.Value.IsSelected).Value; - } - catch { } + var hdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, "HDMI2", this); + var hdmiIn3 = new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, "HDMI3", this); + var hdmiIn4 = new RoutingInputPort(RoutingPortNames.HdmiIn4, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.Hdmi, "HDMI4", this); + var dpIn = new RoutingInputPort(RoutingPortNames.DisplayPortIn, eRoutingSignalType.AudioVideo, + eRoutingPortConnectionType.DisplayPort, "DP", this); + InputPorts.AddRange(new[] { hdmiIn1, hdmiIn2, hdmiIn3, hdmiIn4, dpIn }); - if (currentInput != null) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "SetInput: {0}", selector); - currentInput.IsSelected = false; - } + VolumeLevelFeedback = new IntFeedback("volume", () => { return _FakeVolumeLevel; }); + MuteFeedback = new BoolFeedback("muteOn", () => _IsMuted); - if (!Inputs.Items.TryGetValue(selector, out var input)) - return; - - input.IsSelected = true; - - Inputs.CurrentItem = selector; - } - - - #region IBasicVolumeWithFeedback Members - - /// - /// Gets or sets the VolumeLevelFeedback - /// - public IntFeedback VolumeLevelFeedback { get; private set; } - - /// - /// SetVolume method - /// - public void SetVolume(ushort level) - { - _FakeVolumeLevel = level; - VolumeLevelFeedback.InvokeFireUpdate(); - } - - /// - /// MuteOn method - /// - public void MuteOn() - { - _IsMuted = true; - MuteFeedback.InvokeFireUpdate(); - } - - /// - /// MuteOff method - /// - public void MuteOff() - { - _IsMuted = false; - MuteFeedback.InvokeFireUpdate(); - } - - /// - /// Gets or sets the MuteFeedback - /// - public BoolFeedback MuteFeedback { get; private set; } - - - #endregion - - #region IBasicVolumeControls Members - - /// - /// VolumeUp method - /// - public void VolumeUp(bool pressRelease) - { - //while (pressRelease) - //{ - Debug.LogMessage(LogEventLevel.Verbose, this, "Volume Down {0}", pressRelease); - if (pressRelease) - { - var newLevel = _FakeVolumeLevel + VolumeInterval; - SetVolume((ushort)newLevel); - CrestronEnvironment.Sleep(VolumeHeldRepeatInterval); - } - //} - } - - /// - /// VolumeDown method - /// - public void VolumeDown(bool pressRelease) - { - //while (pressRelease) - //{ - Debug.LogMessage(LogEventLevel.Verbose, this, "Volume Up {0}", pressRelease); - if (pressRelease) - { - var newLevel = _FakeVolumeLevel - VolumeInterval; - SetVolume((ushort)newLevel); - CrestronEnvironment.Sleep(VolumeHeldRepeatInterval); - } - //} - } - - /// - /// MuteToggle method - /// - public void MuteToggle() - { - _IsMuted = !_IsMuted; - MuteFeedback.InvokeFireUpdate(); - } - - #endregion - - /// - /// LinkToApi method - /// - public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - LinkDisplayToApi(this, trilist, joinStart, joinMapKey, bridge); - } + WarmupTime = 10000; + CooldownTime = 10000; } /// - /// Represents a MockDisplayFactory + /// PowerOn method /// - public class MockDisplayFactory : EssentialsDeviceFactory + /// + public override void PowerOn() { - /// - /// Initializes a new instance of the MockDisplayFactory class - /// - public MockDisplayFactory() + if (!PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) { - TypeNames = new List() { "mockdisplay", "mockdisplay2" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Mock Display Device"); - return new MockDisplay(dc.Key, dc.Name); + _IsWarmingUp = true; + IsWarmingUpFeedback.InvokeFireUpdate(); + // Fake power-up cycle + WarmupTimer = new CTimer(o => + { + _IsWarmingUp = false; + _PowerIsOn = true; + IsWarmingUpFeedback.InvokeFireUpdate(); + PowerIsOnFeedback.InvokeFireUpdate(); + }, WarmupTime); } } -} \ No newline at end of file + + public override void PowerOff() + { + // If a display has unreliable-power off feedback, just override this and + // remove this check. + if (PowerIsOnFeedback.BoolValue && !_IsWarmingUp && !_IsCoolingDown) + { + _IsCoolingDown = true; + IsCoolingDownFeedback.InvokeFireUpdate(); + // Fake cool-down cycle + CooldownTimer = new CTimer(o => + { + Debug.LogMessage(LogEventLevel.Verbose, "Cooldown timer ending", this); + _IsCoolingDown = false; + IsCoolingDownFeedback.InvokeFireUpdate(); + _PowerIsOn = false; + PowerIsOnFeedback.InvokeFireUpdate(); + }, CooldownTime); + } + } + + public override void PowerToggle() + { + if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) + PowerOff(); + else if (!PowerIsOnFeedback.BoolValue && !IsCoolingDownFeedback.BoolValue) + PowerOn(); + } + + public override void ExecuteSwitch(object selector) + { + try + { + Debug.LogMessage(LogEventLevel.Verbose, "ExecuteSwitch: {0}", this, selector); + + if (!_PowerIsOn) + { + PowerOn(); + } + + if (!Inputs.Items.TryGetValue(selector.ToString(), out var input)) + return; + + Debug.LogMessage(LogEventLevel.Verbose, "Selected input: {input}", this, input.Key); + input.Select(); + + var inputPort = InputPorts.FirstOrDefault(port => + { + Debug.LogMessage(LogEventLevel.Verbose, "Checking input port {inputPort} with selector {portSelector} against {selector}", this, port, port.Selector, selector); + return port.Selector.ToString() == selector.ToString(); + }); + + if (inputPort == null) + { + Debug.LogMessage(LogEventLevel.Verbose, "Unable to find input port for selector {selector}", this, selector); + return; + } + + Debug.LogMessage(LogEventLevel.Verbose, "Setting current input port to {inputPort}", this, inputPort); + CurrentInputPort = inputPort; + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error making switch: {Exception}", this, ex); + } + } + + public void SetInput(string selector) + { + ISelectableItem currentInput = null; + + + if (currentInput != null) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "SetInput: {0}", selector); + currentInput.IsSelected = false; + } + + if (!Inputs.Items.TryGetValue(selector, out var input)) + return; + + input.IsSelected = true; + + Inputs.CurrentItem = selector; + } + + + #region IBasicVolumeWithFeedback Members + + public IntFeedback VolumeLevelFeedback { get; private set; } + + /// + /// SetVolume method + /// + public void SetVolume(ushort level) + { + _FakeVolumeLevel = level; + VolumeLevelFeedback.InvokeFireUpdate(); + } + + /// + /// MuteOn method + /// + public void MuteOn() + { + _IsMuted = true; + MuteFeedback.InvokeFireUpdate(); + } + + /// + /// MuteOff method + /// + public void MuteOff() + { + _IsMuted = false; + MuteFeedback.InvokeFireUpdate(); + } + + /// + /// Gets or sets the MuteFeedback + /// + public BoolFeedback MuteFeedback { get; private set; } + + + #endregion + + #region IBasicVolumeControls Members + + public void VolumeUp(bool pressRelease) + { + //while (pressRelease) + //{ + Debug.LogMessage(LogEventLevel.Verbose, this, "Volume Down {0}", pressRelease); + if (pressRelease) + { + var newLevel = _FakeVolumeLevel + VolumeInterval; + SetVolume((ushort)newLevel); + CrestronEnvironment.Sleep(VolumeHeldRepeatInterval); + } + //} + } + + public void VolumeDown(bool pressRelease) + { + //while (pressRelease) + //{ + Debug.LogMessage(LogEventLevel.Verbose, this, "Volume Up {0}", pressRelease); + if (pressRelease) + { + var newLevel = _FakeVolumeLevel - VolumeInterval; + SetVolume((ushort)newLevel); + CrestronEnvironment.Sleep(VolumeHeldRepeatInterval); + } + //} + } + + /// + /// MuteToggle method + /// + public void MuteToggle() + { + _IsMuted = !_IsMuted; + MuteFeedback.InvokeFireUpdate(); + } + + #endregion + + public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + LinkDisplayToApi(this, trilist, joinStart, joinMapKey, bridge); + } +} + +/// +/// Represents a MockDisplayFactory +/// +public class MockDisplayFactory : EssentialsDeviceFactory +{ + /// + /// Initializes a new instance of the MockDisplayFactory class + /// + public MockDisplayFactory() + { + TypeNames = new List() { "mockdisplay", "mockdisplay2" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Mock Display Device"); + return new MockDisplay(dc.Key, dc.Name); + } +} diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplayInputs.cs b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplayInputs.cs index b30963ac..77155a67 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplayInputs.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplayInputs.cs @@ -2,135 +2,95 @@ using System.Collections.Generic; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials.Devices.Common.Displays +namespace PepperDash.Essentials.Devices.Common.Displays; + +public class MockDisplayInputs : ISelectableItems { - /// - /// Represents a MockDisplayInputs - /// - public class MockDisplayInputs : ISelectableItems + private Dictionary _items; + + public Dictionary Items { - private Dictionary _items; - - /// - /// Gets or sets the collection of selectable items - /// - public Dictionary Items + get { - get - { - return _items; - } - set - { - if (_items == value) - return; - - _items = value; - - ItemsUpdated?.Invoke(this, null); - } + return _items; } - - private string _currentItem; - - /// - /// Gets or sets the currently selected item - /// - public string CurrentItem + set { - get - { - return _currentItem; - } - set - { - if (_currentItem == value) - return; + if (_items == value) + return; - _currentItem = value; + _items = value; - CurrentItemChanged?.Invoke(this, null); - } + ItemsUpdated?.Invoke(this, null); } - - /// - /// Occurs when the items collection is updated - /// - public event EventHandler ItemsUpdated; - /// - /// Occurs when the current item changes - /// - public event EventHandler CurrentItemChanged; } - /// - /// Represents a MockDisplayInput - /// - public class MockDisplayInput : ISelectableItem + private string _currentItem; + + public string CurrentItem + { + get + { + return _currentItem; + } + set + { + if (_currentItem == value) + return; + + _currentItem = value; + + CurrentItemChanged?.Invoke(this, null); + } + } + + public event EventHandler ItemsUpdated; + public event EventHandler CurrentItemChanged; +} + +public class MockDisplayInput : ISelectableItem +{ + private MockDisplay _parent; + + private bool _isSelected; + + public bool IsSelected { - private MockDisplay _parent; - - private bool _isSelected; - - /// - /// Gets or sets a value indicating whether this input is selected - /// - public bool IsSelected + get { - get - { - return _isSelected; - } - set - { - if (_isSelected == value) - return; - - _isSelected = value; - - ItemUpdated?.Invoke(this, null); - } + return _isSelected; } - - /// - /// Gets or sets the Name - /// - public string Name { get; set; } - - /// - /// Gets or sets the Key - /// - public string Key { get; set; } - - /// - /// Occurs when this item is updated - /// - public event EventHandler ItemUpdated; - - /// - /// Initializes a new instance of the MockDisplayInput class - /// - /// The input key - /// The input name - /// The parent mock display - public MockDisplayInput(string key, string name, MockDisplay parent) + set { - Key = key; - Name = name; - _parent = parent; + if (_isSelected == value) + return; + + _isSelected = value; + + ItemUpdated?.Invoke(this, null); } + } - /// - /// Select method - /// - public void Select() + public string Name { get; set; } + + public string Key { get; set; } + + public event EventHandler ItemUpdated; + + public MockDisplayInput(string key, string name, MockDisplay parent) + { + Key = key; + Name = name; + _parent = parent; + } + + public void Select() + { + if (!_parent.PowerIsOnFeedback.BoolValue) _parent.PowerOn(); + + foreach(var input in _parent.Inputs.Items) { - if (!_parent.PowerIsOnFeedback.BoolValue) _parent.PowerOn(); - - foreach (var input in _parent.Inputs.Items) - { - input.Value.IsSelected = input.Key == this.Key; - } + input.Value.IsSelected = input.Key == this.Key; } } } diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index 80c1f8ee..7f7779c5 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -434,7 +434,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades /// /// Factory for ScreenLiftController devices /// - public class ScreenLiftControllerFactory : EssentialsDeviceFactory + public class ScreenLiftControllerFactory : EssentialsDeviceFactory { /// /// Constructor for ScreenLiftControllerFactory @@ -453,4 +453,4 @@ namespace PepperDash.Essentials.Devices.Common.Shades return new ScreenLiftController(dc.Key, dc.Name, props); } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index 64617770..cdf214bf 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -7,152 +7,154 @@ using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Routing; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Generic +namespace PepperDash.Essentials.Devices.Common.Generic; + +/// +/// Represents a GenericSink +/// +public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort, ICurrentSources { + /// + public Dictionary CurrentSources { get; private set; } + + /// + public Dictionary CurrentSourceKeys { get; private set; } + + /// + public event EventHandler CurrentSourcesChanged; + /// - /// Represents a GenericSink + /// Initializes a new instance of the GenericSink class /// - public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort, ICurrentSources + /// The device key + /// The device name + public GenericSink(string key, string name) : base(key, name) { - /// - public Dictionary CurrentSources { get; private set; } + InputPorts = new RoutingPortCollection(); - /// - public Dictionary CurrentSourceKeys { get; private set; } + var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this); - /// - public event EventHandler CurrentSourcesChanged; + InputPorts.Add(inputPort); - /// - /// Initializes a new instance of the GenericSink class - /// - /// The device key - /// The device name - public GenericSink(string key, string name) : base(key, name) - { - InputPorts = new RoutingPortCollection(); - - var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this); - - InputPorts.Add(inputPort); - - CurrentSources = new Dictionary + CurrentSources = new Dictionary { { eRoutingSignalType.Audio, null }, { eRoutingSignalType.Video, null }, }; - CurrentSourceKeys = new Dictionary + CurrentSourceKeys = new Dictionary { { eRoutingSignalType.Audio, string.Empty }, { eRoutingSignalType.Video, string.Empty }, }; - } + } - /// - public void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + /// + public void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + { + foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) { - foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) + var flagValue = Convert.ToInt32(type); + // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). + // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. + if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) { - var flagValue = Convert.ToInt32(type); - // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). - // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. - if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) - { - this.LogDebug("Skipping {type}", type); - continue; - } - - this.LogDebug("setting {type}", type); - - if (signalType.HasFlag(type)) - { - UpdateCurrentSources(type, sourceListKey, sourceListItem); - } + this.LogDebug("Skipping {type}", type); + continue; } - // Raise the CurrentSourcesChanged event - CurrentSourcesChanged?.Invoke(this, EventArgs.Empty); - } - private void UpdateCurrentSources(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) - { - CurrentSources[signalType] = sourceListItem; - CurrentSourceKeys[signalType] = sourceListKey; - } + this.LogDebug("setting {type}", type); - /// - /// Gets or sets the InputPorts - /// - public RoutingPortCollection InputPorts { get; private set; } - - /// - /// Gets or sets the CurrentSourceInfoKey - /// - public string CurrentSourceInfoKey { get; set; } - - private SourceListItem _currentSource; - /// - /// Gets or sets the CurrentSourceInfo - /// - public SourceListItem CurrentSourceInfo - { - get => _currentSource; - set + if (signalType.HasFlag(type)) { - if (value == _currentSource) - { - return; - } - - CurrentSourceChange?.Invoke(_currentSource, ChangeType.WillChange); - - _currentSource = value; - - CurrentSourceChange?.Invoke(_currentSource, ChangeType.DidChange); + UpdateCurrentSources(type, sourceListKey, sourceListItem); } } + // Raise the CurrentSourcesChanged event + CurrentSourcesChanged?.Invoke(this, EventArgs.Empty); + } - /// - /// Gets the current input port - /// - public RoutingInputPort CurrentInputPort => InputPorts[0]; + private void UpdateCurrentSources(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + { + CurrentSources[signalType] = sourceListItem; + CurrentSourceKeys[signalType] = sourceListKey; + } - /// - /// Event fired when the current source changes - /// - public event SourceInfoChangeHandler CurrentSourceChange; + /// + /// Gets or sets the InputPorts + /// + public RoutingPortCollection InputPorts { get; private set; } - /// - public event InputChangedEventHandler InputChanged; + /// + /// Gets or sets the CurrentSourceInfoKey + /// + public string CurrentSourceInfoKey { get; set; } - /// - public void ExecuteSwitch(object inputSelector) + private SourceListItem _currentSource; + /// + /// Gets or sets the CurrentSourceInfo + /// + public SourceListItem CurrentSourceInfo + { + get => _currentSource; + set { - this.LogDebug("GenericSink Executing Switch to: {inputSelector}", inputSelector); + if (value == _currentSource) + { + return; + } + + CurrentSourceChange?.Invoke(_currentSource, ChangeType.WillChange); + + _currentSource = value; + + CurrentSourceChange?.Invoke(_currentSource, ChangeType.DidChange); } } /// - /// Represents a GenericSinkFactory + /// Gets the current input port /// - public class GenericSinkFactory : EssentialsDeviceFactory - { - /// - /// Initializes a new instance of the GenericSinkFactory class - /// - public GenericSinkFactory() - { - TypeNames = new List() { "genericsink", "genericdestination" }; - } + public RoutingInputPort CurrentInputPort => InputPorts[0]; - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Sink Device"); - return new GenericSink(dc.Key, dc.Name); - } + /// + /// Event fired when the current source changes + /// + public event SourceInfoChangeHandler CurrentSourceChange; + + /// + public event InputChangedEventHandler InputChanged; + + /// + public void ExecuteSwitch(object inputSelector) + { + this.LogDebug("GenericSink Executing Switch to: {inputSelector}", inputSelector); } } + +/// +/// Represents a GenericSinkFactory +/// +public class GenericSinkFactory : EssentialsDeviceFactory +{ + /// + /// Initializes a new instance of the GenericSinkFactory class + /// + public GenericSinkFactory() + { + TypeNames = new List() { "genericsink", "genericdestination" }; + } + + /// + /// BuildDevice method + /// + /// + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Sink Device"); + return new GenericSink(dc.Key, dc.Name); + } +} + + + diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSource.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSource.cs index 3d0f1304..9926f31b 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSource.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSource.cs @@ -5,77 +5,76 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common +namespace PepperDash.Essentials.Devices.Common; + +/// +/// Represents a GenericSource +/// +public class GenericSource : EssentialsDevice, IUiDisplayInfo, IRoutingSource, IUsageTracking +{ + + /// + /// Gets or sets the DisplayUiType + /// + public uint DisplayUiType { get { return DisplayUiConstants.TypeNoControls; } } + + /// + /// Initializes a new instance of the GenericSource class + /// + /// The device key + /// The device name + public GenericSource(string key, string name) + : base(key, name) + { + + AnyOut = new RoutingOutputPort(RoutingPortNames.AnyOut, eRoutingSignalType.Audio | eRoutingSignalType.Video, + eRoutingPortConnectionType.Hdmi, null, this); + OutputPorts = new RoutingPortCollection { AnyOut }; + } + + #region IRoutingOutputs Members + + /// + /// Gets or sets the AnyOut + /// + public RoutingOutputPort AnyOut { get; private set; } + /// + /// Gets or sets the OutputPorts + /// + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + #region IUsageTracking Members + + /// + /// Gets or sets the UsageTracker + /// + public UsageTracking UsageTracker { get; set; } + + #endregion +} + +/// +/// Represents a GenericSourceFactory +/// +public class GenericSourceFactory : EssentialsDeviceFactory { /// - /// Represents a GenericSource + /// Initializes a new instance of the GenericSourceFactory class /// - public class GenericSource : EssentialsDevice, IUiDisplayInfo, IRoutingSource, IUsageTracking + public GenericSourceFactory() { - - /// - /// Gets or sets the DisplayUiType - /// - public uint DisplayUiType { get { return DisplayUiConstants.TypeNoControls; } } - - /// - /// Initializes a new instance of the GenericSource class - /// - /// The device key - /// The device name - public GenericSource(string key, string name) - : base(key, name) - { - - AnyOut = new RoutingOutputPort(RoutingPortNames.AnyOut, eRoutingSignalType.Audio | eRoutingSignalType.Video, - eRoutingPortConnectionType.Hdmi, null, this); - OutputPorts = new RoutingPortCollection { AnyOut }; - } - - #region IRoutingOutputs Members - - /// - /// Gets or sets the AnyOut - /// - public RoutingOutputPort AnyOut { get; private set; } - /// - /// Gets or sets the OutputPorts - /// - public RoutingPortCollection OutputPorts { get; private set; } - - #endregion - - #region IUsageTracking Members - - /// - /// Gets or sets the UsageTracker - /// - public UsageTracking UsageTracker { get; set; } - - #endregion + TypeNames = new List() { "genericsource" }; } /// - /// Represents a GenericSourceFactory + /// BuildDevice method /// - public class GenericSourceFactory : EssentialsDeviceFactory + /// + public override EssentialsDevice BuildDevice(DeviceConfig dc) { - /// - /// Initializes a new instance of the GenericSourceFactory class - /// - public GenericSourceFactory() - { - TypeNames = new List() { "genericsource" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Source Device"); - return new GenericSource(dc.Key, dc.Name); - } + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Source Device"); + return new GenericSource(dc.Key, dc.Name); } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Devices.Common/Lighting/LightingBase.cs b/src/PepperDash.Essentials.Devices.Common/Lighting/LightingBase.cs index 9dd3b816..a77b936b 100644 --- a/src/PepperDash.Essentials.Devices.Common/Lighting/LightingBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Lighting/LightingBase.cs @@ -1,6 +1,4 @@ - - -using System; +using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharpPro.DeviceSupport; @@ -11,167 +9,167 @@ using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Lighting; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Lighting +namespace PepperDash.Essentials.Devices.Common.Lighting; + + +/// +/// Base class for lighting devices that support scenes +/// +public abstract class LightingBase : EssentialsBridgeableDevice, ILightingScenes { + #region ILightingScenes Members + /// - /// Base class for lighting devices that support scenes + /// Event fired when lighting scene changes /// - public abstract class LightingBase : EssentialsBridgeableDevice, ILightingScenes + public event EventHandler LightingSceneChange; + + /// + /// Gets or sets the LightingScenes + /// + public List LightingScenes { get; protected set; } + + /// + /// Gets or sets the CurrentLightingScene + /// + public LightingScene CurrentLightingScene { get; protected set; } + + /// + /// Gets or sets the CurrentLightingSceneFeedback + /// + public IntFeedback CurrentLightingSceneFeedback { get; protected set; } + + #endregion + + /// + /// Initializes a new instance of the LightingBase class + /// + /// The device key + /// The device name + protected LightingBase(string key, string name) + : base(key, name) { - #region ILightingScenes Members + LightingScenes = new List(); - /// - /// Event fired when lighting scene changes - /// - public event EventHandler LightingSceneChange; + CurrentLightingScene = new LightingScene(); + //CurrentLightingSceneFeedback = new IntFeedback(() => { return int.Parse(this.CurrentLightingScene.ID); }); + } - /// - /// Gets or sets the LightingScenes - /// - public List LightingScenes { get; protected set; } + /// + /// Selects the specified lighting scene + /// + /// The lighting scene to select + public abstract void SelectScene(LightingScene scene); - /// - /// Gets or sets the CurrentLightingScene - /// - public LightingScene CurrentLightingScene { get; protected set; } + /// + /// SimulateSceneSelect method + /// + public void SimulateSceneSelect(string sceneName) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Simulating selection of scene '{0}'", sceneName); - /// - /// Gets or sets the CurrentLightingSceneFeedback - /// - public IntFeedback CurrentLightingSceneFeedback { get; protected set; } + var scene = LightingScenes.FirstOrDefault(s => s.Name.Equals(sceneName)); - #endregion - - /// - /// Initializes a new instance of the LightingBase class - /// - /// The device key - /// The device name - protected LightingBase(string key, string name) - : base(key, name) + if (scene != null) { - LightingScenes = new List(); - - CurrentLightingScene = new LightingScene(); - //CurrentLightingSceneFeedback = new IntFeedback(() => { return int.Parse(this.CurrentLightingScene.ID); }); + CurrentLightingScene = scene; + OnLightingSceneChange(); } + } - /// - /// Selects the specified lighting scene - /// - /// The lighting scene to select - public abstract void SelectScene(LightingScene scene); - - /// - /// SimulateSceneSelect method - /// - public void SimulateSceneSelect(string sceneName) + /// + /// Sets the IsActive property on each scene and fires the LightingSceneChange event + /// + protected void OnLightingSceneChange() + { + foreach (var scene in LightingScenes) { - Debug.LogMessage(LogEventLevel.Debug, this, "Simulating selection of scene '{0}'", sceneName); + if (scene == CurrentLightingScene) + scene.IsActive = true; - var scene = LightingScenes.FirstOrDefault(s => s.Name.Equals(sceneName)); - - if (scene != null) - { - CurrentLightingScene = scene; - OnLightingSceneChange(); - } - } - - /// - /// Sets the IsActive property on each scene and fires the LightingSceneChange event - /// - protected void OnLightingSceneChange() - { - foreach (var scene in LightingScenes) - { - if (scene == CurrentLightingScene) - scene.IsActive = true; - - else - scene.IsActive = false; - } - LightingSceneChange?.Invoke(this, new LightingSceneChangeEventArgs(CurrentLightingScene)); - } - - /// - /// Links the lighting device to API with join map configuration - /// - /// The lighting device to link - /// The trilist to link to - /// The starting join number - /// The join map key - /// The EISC API bridge - /// The configured join map - protected GenericLightingJoinMap LinkLightingToApi(LightingBase lightingDevice, BasicTriList trilist, uint joinStart, - string joinMapKey, EiscApiAdvanced bridge) - { - var joinMap = new GenericLightingJoinMap(joinStart); - - var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); - - if (!string.IsNullOrEmpty(joinMapSerialized)) - joinMap = JsonConvert.DeserializeObject(joinMapSerialized); - - if (bridge != null) - { - bridge.AddJoinMap(Key, joinMap); - } else - { - Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); - } + scene.IsActive = false; + } + LightingSceneChange?.Invoke(this, new LightingSceneChangeEventArgs(CurrentLightingScene)); + } - return LinkLightingToApi(lightingDevice, trilist, joinMap); + /// + /// Links the lighting device to API with join map configuration + /// + /// The lighting device to link + /// The trilist to link to + /// The starting join number + /// The join map key + /// The EISC API bridge + /// The configured join map + protected GenericLightingJoinMap LinkLightingToApi(LightingBase lightingDevice, BasicTriList trilist, uint joinStart, + string joinMapKey, EiscApiAdvanced bridge) + { + var joinMap = new GenericLightingJoinMap(joinStart); + + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + + if (bridge != null) + { + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); } - /// - /// Links the lighting device to API using an existing join map - /// - /// The lighting device to link - /// The trilist to link to - /// The join map to use - /// The join map used for linking - protected GenericLightingJoinMap LinkLightingToApi(LightingBase lightingDevice, BasicTriList trilist, GenericLightingJoinMap joinMap) + return LinkLightingToApi(lightingDevice, trilist, joinMap); + } + + /// + /// Links the lighting device to API using an existing join map + /// + /// The lighting device to link + /// The trilist to link to + /// The join map to use + /// The join map used for linking + protected GenericLightingJoinMap LinkLightingToApi(LightingBase lightingDevice, BasicTriList trilist, GenericLightingJoinMap joinMap) + { + Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); + + Debug.LogMessage(LogEventLevel.Information, "Linking to Lighting Type {0}", lightingDevice.GetType().Name.ToString()); + + // GenericLighitng Actions & FeedBack + trilist.SetUShortSigAction(joinMap.SelectScene.JoinNumber, u => lightingDevice.SelectScene(lightingDevice.LightingScenes[u])); + + var sceneIndex = 0; + foreach (var scene in lightingDevice.LightingScenes) { - Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); + var index = sceneIndex; - Debug.LogMessage(LogEventLevel.Information, "Linking to Lighting Type {0}", lightingDevice.GetType().Name.ToString()); + trilist.SetSigTrueAction((uint)(joinMap.SelectSceneDirect.JoinNumber + index), () => lightingDevice.SelectScene(lightingDevice.LightingScenes[index])); + scene.IsActiveFeedback.LinkInputSig(trilist.BooleanInput[(uint)(joinMap.SelectSceneDirect.JoinNumber + index)]); + trilist.StringInput[(uint)(joinMap.SelectSceneDirect.JoinNumber + index)].StringValue = scene.Name; + trilist.BooleanInput[(uint)(joinMap.ButtonVisibility.JoinNumber + index)].BoolValue = true; - // GenericLighitng Actions & FeedBack - trilist.SetUShortSigAction(joinMap.SelectScene.JoinNumber, u => lightingDevice.SelectScene(lightingDevice.LightingScenes[u])); + sceneIndex++; + } - var sceneIndex = 0; + trilist.OnlineStatusChange += (sender, args) => + { + if (!args.DeviceOnLine) return; + + sceneIndex = 0; foreach (var scene in lightingDevice.LightingScenes) { var index = sceneIndex; - trilist.SetSigTrueAction((uint)(joinMap.SelectSceneDirect.JoinNumber + index), () => lightingDevice.SelectScene(lightingDevice.LightingScenes[index])); - scene.IsActiveFeedback.LinkInputSig(trilist.BooleanInput[(uint)(joinMap.SelectSceneDirect.JoinNumber + index)]); trilist.StringInput[(uint)(joinMap.SelectSceneDirect.JoinNumber + index)].StringValue = scene.Name; trilist.BooleanInput[(uint)(joinMap.ButtonVisibility.JoinNumber + index)].BoolValue = true; + scene.IsActiveFeedback.FireUpdate(); sceneIndex++; } + }; - trilist.OnlineStatusChange += (sender, args) => - { - if (!args.DeviceOnLine) return; - - sceneIndex = 0; - foreach (var scene in lightingDevice.LightingScenes) - { - var index = sceneIndex; - - trilist.StringInput[(uint)(joinMap.SelectSceneDirect.JoinNumber + index)].StringValue = scene.Name; - trilist.BooleanInput[(uint)(joinMap.ButtonVisibility.JoinNumber + index)].BoolValue = true; - scene.IsActiveFeedback.FireUpdate(); - - sceneIndex++; - } - }; - - return joinMap; - } + return joinMap; } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleSpaceRoom.cs b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleSpaceRoom.cs index ecce473e..6a1a816d 100644 --- a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleSpaceRoom.cs +++ b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleSpaceRoom.cs @@ -1,28 +1,27 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Room.Config; -namespace PepperDash.Essentials.Devices.Common.Room +namespace PepperDash.Essentials.Devices.Common.Room; + +/// +/// Defines the contract for IEssentialsHuddleSpaceRoom +/// +public interface IEssentialsHuddleSpaceRoom : IEssentialsRoom, IHasCurrentSourceInfoChange, IRunRouteAction, IHasDefaultDisplay, IHasCurrentVolumeControls, IRoomOccupancy, + IEmergency, IMicrophonePrivacy { /// - /// Defines the contract for IEssentialsHuddleSpaceRoom + /// Gets whether to exclude this room from global functions /// - public interface IEssentialsHuddleSpaceRoom : IEssentialsRoom, IHasCurrentSourceInfoChange, IRunRouteAction, IHasDefaultDisplay, IHasCurrentVolumeControls, IRoomOccupancy, - IEmergency, IMicrophonePrivacy - { - /// - /// Gets whether to exclude this room from global functions - /// - bool ExcludeFromGlobalFunctions { get; } + bool ExcludeFromGlobalFunctions { get; } - /// - /// Runs the route action for the given routeKey and sourceListKey - /// - /// The route key - void RunRouteAction(string routeKey); + /// + /// Runs the route action for the given routeKey and sourceListKey + /// + /// The route key + void RunRouteAction(string routeKey); - /// - /// Gets the PropertiesConfig - /// - EssentialsHuddleRoomPropertiesConfig PropertiesConfig { get; } - } -} \ No newline at end of file + /// + /// Gets the PropertiesConfig + /// + EssentialsHuddleRoomPropertiesConfig PropertiesConfig { get; } +} diff --git a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleVtc1Room.cs b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleVtc1Room.cs index 5547f595..728e1e1b 100644 --- a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleVtc1Room.cs +++ b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleVtc1Room.cs @@ -1,51 +1,53 @@ -using PepperDash.Essentials.Core; +using System; +using PepperDash.Essentials.Core; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Devices.Common.VideoCodec; using PepperDash.Essentials.Room.Config; -namespace PepperDash.Essentials.Devices.Common.Room +namespace PepperDash.Essentials.Devices.Common.Room; + + +/// +/// Defines the contract for IEssentialsHuddleVtc1Room +/// +[Obsolete("Obsolete in favor of IEssentialsRoom. This interface will be removed in a future release.")] +public interface IEssentialsHuddleVtc1Room : IEssentialsRoom, IHasCurrentSourceInfoChange, IHasCurrentVolumeControls, IRunRouteAction, IRunDefaultCallRoute, IHasVideoCodec, IHasAudioCodec, IHasDefaultDisplay, IHasInCallFeedback, + IRoomOccupancy, IEmergency, IMicrophonePrivacy { /// - /// Defines the contract for IEssentialsHuddleVtc1Room + /// Gets the PropertiesConfig /// - public interface IEssentialsHuddleVtc1Room : IEssentialsRoom, IHasCurrentSourceInfoChange, IHasCurrentVolumeControls, IRunRouteAction, IRunDefaultCallRoute, IHasVideoCodec, IHasAudioCodec, IHasDefaultDisplay, IHasInCallFeedback, - IRoomOccupancy, IEmergency, IMicrophonePrivacy - { - /// - /// Gets the PropertiesConfig - /// - EssentialsHuddleVtc1PropertiesConfig PropertiesConfig { get; } + EssentialsHuddleVtc1PropertiesConfig PropertiesConfig { get; } - /// - /// Gets whether to exclude this room from global functions - /// - bool ExcludeFromGlobalFunctions { get; } + /// + /// Gets whether to exclude this room from global functions + /// + bool ExcludeFromGlobalFunctions { get; } - /// - /// Runs the route action for the given routeKey and sourceListKey - /// - /// The route key - void RunRouteAction(string routeKey); + /// + /// Runs the route action for the given routeKey and sourceListKey + /// + /// The route key + void RunRouteAction(string routeKey); - /// - /// Gets the ScheduleSource - /// - IHasScheduleAwareness ScheduleSource { get; } + /// + /// Gets the ScheduleSource + /// + IHasScheduleAwareness ScheduleSource { get; } - /// - /// Gets the InCallFeedback - /// - new BoolFeedback InCallFeedback { get; } + /// + /// Gets the InCallFeedback + /// + new BoolFeedback InCallFeedback { get; } - /// - /// Gets the PrivacyModeIsOnFeedback - /// - new BoolFeedback PrivacyModeIsOnFeedback { get; } + /// + /// Gets the PrivacyModeIsOnFeedback + /// + new BoolFeedback PrivacyModeIsOnFeedback { get; } - /// - /// Gets the DefaultCodecRouteString - /// - string DefaultCodecRouteString { get; } - } -} \ No newline at end of file + /// + /// Gets the DefaultCodecRouteString + /// + string DefaultCodecRouteString { get; } +} diff --git a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsRoomPropertiesConfig.cs b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsRoomPropertiesConfig.cs index e45be6e0..7b6db77e 100644 --- a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsRoomPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsRoomPropertiesConfig.cs @@ -1,15 +1,16 @@ using PepperDash.Essentials.Room.Config; -namespace PepperDash.Essentials.Devices.Common.Room +namespace PepperDash.Essentials.Devices.Common.Room; + + +/// +/// Defines the contract for IEssentialsRoomPropertiesConfig +/// +public interface IEssentialsRoomPropertiesConfig { /// - /// Defines the contract for IEssentialsRoomPropertiesConfig + /// Gets the PropertiesConfig /// - public interface IEssentialsRoomPropertiesConfig - { - /// - /// Gets the PropertiesConfig - /// - EssentialsRoomPropertiesConfig PropertiesConfig { get; } - } + EssentialsRoomPropertiesConfig PropertiesConfig { get; } } + diff --git a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsTechRoom.cs b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsTechRoom.cs index 9301f37d..2dd79a99 100644 --- a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsTechRoom.cs +++ b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsTechRoom.cs @@ -5,37 +5,36 @@ using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Devices.Common.Displays; using PepperDash.Essentials.Room.Config; +namespace PepperDash.Essentials.Devices.Common.Room; -namespace PepperDash.Essentials.Devices.Common.Room +/// +/// Defines the contract for IEssentialsTechRoom +/// +public interface IEssentialsTechRoom : IEssentialsRoom, ITvPresetsProvider, IBridgeAdvanced, IRunDirectRouteAction { /// - /// Defines the contract for IEssentialsTechRoom + /// Gets the PropertiesConfig /// - public interface IEssentialsTechRoom : IEssentialsRoom, ITvPresetsProvider, IBridgeAdvanced, IRunDirectRouteAction - { - /// - /// Gets the PropertiesConfig - /// - EssentialsTechRoomConfig PropertiesConfig { get; } + EssentialsTechRoomConfig PropertiesConfig { get; } - /// - /// Gets the Tuners - /// - Dictionary Tuners { get; } + /// + /// Gets the Tuners + /// + Dictionary Tuners { get; } - /// - /// Gets the Displays - /// - Dictionary Displays { get; } + /// + /// Gets the Displays + /// + Dictionary Displays { get; } - /// - /// Powers on the room - /// - void RoomPowerOn(); + /// + /// Powers on the room + /// + void RoomPowerOn(); - /// - /// Powers off the room - /// - void RoomPowerOff(); - } + /// + /// Powers off the room + /// + void RoomPowerOff(); } + diff --git a/src/PepperDash.Essentials.Devices.Common/SetTopBox/IRSetTopBoxBase.cs b/src/PepperDash.Essentials.Devices.Common/SetTopBox/IRSetTopBoxBase.cs index 7d49b9b7..6171ef86 100644 --- a/src/PepperDash.Essentials.Devices.Common/SetTopBox/IRSetTopBoxBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/SetTopBox/IRSetTopBoxBase.cs @@ -656,5 +656,5 @@ namespace PepperDash.Essentials.Devices.Common } } } +} -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/SetTopBox/SetTopBoxPropertiesConfig.cs b/src/PepperDash.Essentials.Devices.Common/SetTopBox/SetTopBoxPropertiesConfig.cs index 193b41e7..94c5dd51 100644 --- a/src/PepperDash.Essentials.Devices.Common/SetTopBox/SetTopBoxPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Devices.Common/SetTopBox/SetTopBoxPropertiesConfig.cs @@ -1,36 +1,35 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Devices.Common +namespace PepperDash.Essentials.Devices.Common; + +/// +/// Represents a SetTopBoxPropertiesConfig +/// +public class SetTopBoxPropertiesConfig : PepperDash.Essentials.Core.Config.SourceDevicePropertiesConfigBase { /// - /// Represents a SetTopBoxPropertiesConfig + /// Gets or sets the HasPresets /// - public class SetTopBoxPropertiesConfig : PepperDash.Essentials.Core.Config.SourceDevicePropertiesConfigBase - { - /// - /// Gets or sets the HasPresets - /// - public bool HasPresets { get; set; } - /// - /// Gets or sets the HasDvr - /// - public bool HasDvr { get; set; } - /// - /// Gets or sets the HasDpad - /// - public bool HasDpad { get; set; } - /// - /// Gets or sets the HasNumeric - /// - public bool HasNumeric { get; set; } - /// - /// Gets or sets the IrPulseTime - /// - public int IrPulseTime { get; set; } + public bool HasPresets { get; set; } + /// + /// Gets or sets the HasDvr + /// + public bool HasDvr { get; set; } + /// + /// Gets or sets the HasDpad + /// + public bool HasDpad { get; set; } + /// + /// Gets or sets the HasNumeric + /// + public bool HasNumeric { get; set; } + /// + /// Gets or sets the IrPulseTime + /// + public int IrPulseTime { get; set; } - /// - /// Gets or sets the Control - /// - public ControlPropertiesConfig Control { get; set; } - } -} \ No newline at end of file + /// + /// Gets or sets the Control + /// + public ControlPropertiesConfig Control { get; set; } +} diff --git a/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs b/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs index e0080627..c1e613e2 100644 --- a/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs +++ b/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs @@ -7,181 +7,169 @@ using PepperDash.Essentials.Core.CrestronIO; using PepperDash.Essentials.Core.Shades; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Shades +namespace PepperDash.Essentials.Devices.Common.Shades; + +/// +/// Controls a single shade using three relays +/// +public class RelayControlledShade : ShadeBase, IShadesOpenCloseStop +{ + RelayControlledShadeConfigProperties Config; + + ISwitchedOutput OpenRelay; + ISwitchedOutput StopOrPresetRelay; + ISwitchedOutput CloseRelay; + + int RelayPulseTime; + + /// + /// Gets the label for the stop or preset button, depending on how the shade is configured + /// + public string StopOrPresetButtonLabel { get; set; } + + /// + /// Constructor for RelayControlledShade + /// + /// + /// + /// + public RelayControlledShade(string key, string name, RelayControlledShadeConfigProperties config) + : base(key, name) + { + Config = config; + + RelayPulseTime = Config.RelayPulseTime; + + StopOrPresetButtonLabel = Config.StopOrPresetLabel; + + } + + /// + public override bool CustomActivate() + { + //Create ISwitchedOutput objects based on props + OpenRelay = GetSwitchedOutputFromDevice(Config.Relays.Open); + StopOrPresetRelay = GetSwitchedOutputFromDevice(Config.Relays.StopOrPreset); + CloseRelay = GetSwitchedOutputFromDevice(Config.Relays.Close); + + + return base.CustomActivate(); + } + + /// + public override void Open() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Opening Shade: '{0}'", this.Name); + + PulseOutput(OpenRelay, RelayPulseTime); + } + + /// + + public override void Stop() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Shade: '{0}'", this.Name); + + PulseOutput(StopOrPresetRelay, RelayPulseTime); + } + + /// + public override void Close() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Closing Shade: '{0}'", this.Name); + + PulseOutput(CloseRelay, RelayPulseTime); + } + + void PulseOutput(ISwitchedOutput output, int pulseTime) + { + output.On(); + CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime); + } + + /// + /// Attempts to get the port on the specified device from config + /// + /// + /// + ISwitchedOutput GetSwitchedOutputFromDevice(IOPortConfig relayConfig) + { + var portDevice = DeviceManager.GetDeviceForKey(relayConfig.PortDeviceKey); + + if (portDevice != null) + { + return (portDevice as ISwitchedOutputCollection).SwitchedOutputs[relayConfig.PortNumber]; + } + else + { + Debug.LogMessage(LogEventLevel.Debug, this, "Error: Unable to get relay on port '{0}' from device with key '{1}'", relayConfig.PortNumber, relayConfig.PortDeviceKey); + return null; + } + } + +} + +/// +/// Configuration properties for RelayControlledShade +/// +public class RelayControlledShadeConfigProperties { /// - /// Controls a single shade using three relays + /// The amount of time in milliseconds to pulse the relay for when opening or closing the shade /// - public class RelayControlledShade : ShadeBase, IShadesOpenCloseStop - { - RelayControlledShadeConfigProperties Config; - - ISwitchedOutput OpenRelay; - ISwitchedOutput StopOrPresetRelay; - ISwitchedOutput CloseRelay; - - int RelayPulseTime; - - /// - /// Gets or sets the StopOrPresetButtonLabel - /// - public string StopOrPresetButtonLabel { get; set; } - - /// - /// Initializes a new instance of the RelayControlledShade class - /// - /// The device key - /// The device name - /// The relay controlled shade configuration - public RelayControlledShade(string key, string name, RelayControlledShadeConfigProperties config) - : base(key, name) - { - Config = config; - - RelayPulseTime = Config.RelayPulseTime; - - StopOrPresetButtonLabel = Config.StopOrPresetLabel; - - } - - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() - { - //Create ISwitchedOutput objects based on props - OpenRelay = GetSwitchedOutputFromDevice(Config.Relays.Open); - StopOrPresetRelay = GetSwitchedOutputFromDevice(Config.Relays.StopOrPreset); - CloseRelay = GetSwitchedOutputFromDevice(Config.Relays.Close); - - - return base.CustomActivate(); - } - - /// - /// Open method - /// - /// - public override void Open() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Opening Shade: '{0}'", this.Name); - - PulseOutput(OpenRelay, RelayPulseTime); - } - - /// - /// Stop method - /// - /// - public override void Stop() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Shade: '{0}'", this.Name); - - PulseOutput(StopOrPresetRelay, RelayPulseTime); - } - - /// - /// Close method - /// - /// - public override void Close() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Closing Shade: '{0}'", this.Name); - - PulseOutput(CloseRelay, RelayPulseTime); - } - - void PulseOutput(ISwitchedOutput output, int pulseTime) - { - output.On(); - CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime); - } - - /// - /// Attempts to get the port on teh specified device from config - /// - /// - /// - ISwitchedOutput GetSwitchedOutputFromDevice(IOPortConfig relayConfig) - { - var portDevice = DeviceManager.GetDeviceForKey(relayConfig.PortDeviceKey); - - if (portDevice != null) - { - return (portDevice as ISwitchedOutputCollection).SwitchedOutputs[relayConfig.PortNumber]; - } - else - { - Debug.LogMessage(LogEventLevel.Debug, this, "Error: Unable to get relay on port '{0}' from device with key '{1}'", relayConfig.PortNumber, relayConfig.PortDeviceKey); - return null; - } - } - - } + public int RelayPulseTime { get; set; } /// - /// Represents a RelayControlledShadeConfigProperties + /// The relays that control the shade /// - public class RelayControlledShadeConfigProperties - { - /// - /// Gets or sets the RelayPulseTime - /// - public int RelayPulseTime { get; set; } - /// - /// Gets or sets the Relays - /// - public ShadeRelaysConfig Relays { get; set; } - /// - /// Gets or sets the StopOrPresetLabel - /// - public string StopOrPresetLabel { get; set; } - - /// - /// Represents a ShadeRelaysConfig - /// - public class ShadeRelaysConfig - { - /// - /// Gets or sets the Open - /// - public IOPortConfig Open { get; set; } - /// - /// Gets or sets the StopOrPreset - /// - public IOPortConfig StopOrPreset { get; set; } - /// - /// Gets or sets the Close - /// - public IOPortConfig Close { get; set; } - } - } + public ShadeRelaysConfig Relays { get; set; } /// - /// Represents a RelayControlledShadeFactory + /// The label for the stop or preset button, depending on how the shade is configured /// - public class RelayControlledShadeFactory : EssentialsDeviceFactory + public string StopOrPresetLabel { get; set; } + + /// + /// Configuration for the relays that control the shade + /// + public class ShadeRelaysConfig { /// - /// Initializes a new instance of the RelayControlledShadeFactory class + /// The relay that opens the shade /// - public RelayControlledShadeFactory() - { - TypeNames = new List() { "relaycontrolledshade" }; - } + public IOPortConfig Open { get; set; } /// - /// BuildDevice method + /// The relay that stops the shade or presets the shade to a certain position, depending on how the shade is configured /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device"); - var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); + public IOPortConfig StopOrPreset { get; set; } - return new RelayControlledShade(dc.Key, dc.Name, props); - } + /// + /// The relay that closes the shade + /// + public IOPortConfig Close { get; set; } + } +} + +/// +/// Factory for creating RelayControlledShade devices +/// +public class RelayControlledShadeFactory : EssentialsDeviceFactory +{ + /// + /// Constructor for RelayControlledShadeFactory + /// + public RelayControlledShadeFactory() + { + TypeNames = new List() { "relaycontrolledshade" }; } + /// + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device"); + var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); + + return new RelayControlledShade(dc.Key, dc.Name, props); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Shades/ShadeBase.cs b/src/PepperDash.Essentials.Devices.Common/Shades/ShadeBase.cs index ba3d67dd..6b8e13b8 100644 --- a/src/PepperDash.Essentials.Devices.Common/Shades/ShadeBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Shades/ShadeBase.cs @@ -1,39 +1,40 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Shades; -namespace PepperDash.Essentials.Devices.Common.Shades +namespace PepperDash.Essentials.Devices.Common.Shades; + + +/// +/// Base class for shade devices +/// +public abstract class ShadeBase : EssentialsDevice, IShadesOpenCloseStop { /// - /// Base class for shade devices + /// Initializes a new instance of the ShadeBase class /// - public abstract class ShadeBase : EssentialsDevice, IShadesOpenCloseStop + /// The device key + /// The device name + public ShadeBase(string key, string name) + : base(key, name) { - /// - /// Initializes a new instance of the ShadeBase class - /// - /// The device key - /// The device name - public ShadeBase(string key, string name) - : base(key, name) - { - } - - #region iShadesOpenClose Members - - /// - /// Opens the shade - /// - public abstract void Open(); - /// - /// Stops the shade - /// - public abstract void Stop(); - /// - /// Closes the shade - /// - public abstract void Close(); - - #endregion } + + #region iShadesOpenClose Members + + /// + /// Opens the shade + /// + public abstract void Open(); + /// + /// Stops the shade + /// + public abstract void Stop(); + /// + /// Closes the shade + /// + public abstract void Close(); + + #endregion } + diff --git a/src/PepperDash.Essentials.Devices.Common/Shades/ShadeController.cs b/src/PepperDash.Essentials.Devices.Common/Shades/ShadeController.cs index dc8cb125..7d22ec56 100644 --- a/src/PepperDash.Essentials.Devices.Common/Shades/ShadeController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Shades/ShadeController.cs @@ -5,105 +5,96 @@ using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Shades; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Shades +namespace PepperDash.Essentials.Devices.Common.Shades; + +/// +/// Class that contains the shades to be controlled in a room +/// +public class ShadeController : EssentialsDevice, IShades +{ + ShadeControllerConfigProperties Config; + + /// + /// List of shades to be controlled by this controller + /// + public List Shades { get; private set; } + + /// + /// Constructor for ShadeController + /// + /// + /// + /// + public ShadeController(string key, string name, ShadeControllerConfigProperties config) + : base(key, name) + { + Config = config; + + Shades = new List(); + } + + /// + public override bool CustomActivate() + { + foreach (var shadeConfig in Config.Shades) + { + var shade = DeviceManager.GetDeviceForKey(shadeConfig.Key) as ShadeBase; + + if (shade != null) + { + AddShade(shade); + } + } + return base.CustomActivate(); + } + + void AddShade(IShadesOpenCloseStop shade) + { + Shades.Add(shade); + } +} + +/// +/// Class representing the properties for the ShadeController device, including a list of shades to control +/// +public class ShadeControllerConfigProperties { /// - /// Class that contains the shades to be controlled in a room + /// List of shades to control, represented by their unique keys /// - public class ShadeController : EssentialsDevice, IShades - { - ShadeControllerConfigProperties Config; - - /// - /// Gets the collection of shades controlled by this controller - /// - public List Shades { get; private set; } - - /// - /// Initializes a new instance of the ShadeController class - /// - /// The device key - /// The device name - /// The shade controller configuration - public ShadeController(string key, string name, ShadeControllerConfigProperties config) - : base(key, name) - { - Config = config; - - Shades = new List(); - } - - /// - /// CustomActivate method - /// - /// - public override bool CustomActivate() - { - foreach (var shadeConfig in Config.Shades) - { - var shade = DeviceManager.GetDeviceForKey(shadeConfig.Key) as ShadeBase; - - if (shade != null) - { - AddShade(shade); - } - } - return base.CustomActivate(); - } - - void AddShade(IShadesOpenCloseStop shade) - { - Shades.Add(shade); - } - } + public List Shades { get; set; } /// - /// Represents a ShadeControllerConfigProperties + /// Class representing the configuration for an individual shade, including its unique key /// - public class ShadeControllerConfigProperties + public class ShadeConfig : IKeyed { /// - /// Gets or sets the Shades + /// The unique key of the shade device to be controlled /// - public List Shades { get; set; } - - - /// - /// Represents a ShadeConfig - /// - public class ShadeConfig - { - /// - /// Gets or sets the Key - /// - public string Key { get; set; } - } + public string Key { get; set; } } +} +/// +/// Factory for creating ShadeController devices +/// +public class ShadeControllerFactory : EssentialsDeviceFactory +{ /// - /// Represents a ShadeControllerFactory + /// Constructor for ShadeControllerFactory /// - public class ShadeControllerFactory : EssentialsDeviceFactory + public ShadeControllerFactory() { - /// - /// Initializes a new instance of the ShadeControllerFactory class - /// - public ShadeControllerFactory() - { - TypeNames = new List() { "shadecontroller" }; - } - - /// - /// BuildDevice method - /// - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new ShadeController Device"); - var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); - - return new ShadeController(dc.Key, dc.Name, props); - } + TypeNames = new List() { "shadecontroller" }; } + /// + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new ShadeController Device"); + var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString()); + + return new ShadeController(dc.Key, dc.Name, props); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs index afe47814..9483a6c5 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs @@ -1,6 +1,6 @@ using System; +using System.Collections.Generic; using Crestron.SimplSharp; - using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; @@ -8,188 +8,157 @@ using PepperDash.Essentials.Devices.Common.Sources; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.SoftCodec +namespace PepperDash.Essentials.Devices.Common.SoftCodec; + +public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink { - /// - /// Represents a BlueJeansPc - /// - public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink + + public RoutingInputPort AnyVideoIn { get; private set; } + + public RoutingInputPort CurrentInputPort => AnyVideoIn; + + #region IRoutingInputs Members + + public RoutingPortCollection InputPorts { get; private set; } + + #endregion + + public BlueJeansPc(string key, string name) + : base(key, name) { - - /// - /// Gets or sets the AnyVideoIn - /// - public RoutingInputPort AnyVideoIn { get; private set; } - - /// - /// Gets the CurrentInputPort - /// - public RoutingInputPort CurrentInputPort => AnyVideoIn; - - #region IRoutingInputs Members - - /// - /// Gets or sets the InputPorts - /// - public RoutingPortCollection InputPorts { get; private set; } - - #endregion - - /// - /// Initializes a new instance of the class - /// - /// The device key - /// The device name - public BlueJeansPc(string key, string name) - : base(key, name) + InputPorts = new RoutingPortCollection { - InputPorts = new RoutingPortCollection - { - (AnyVideoIn = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this)) - }; - } - - #region IRunRouteAction Members - - /// - /// RunRouteAction method - /// - public void RunRouteAction(string routeKey, string sourceListKey) - { - RunRouteAction(routeKey, sourceListKey, null); - } - - /// - /// RunRouteAction method - /// - public void RunRouteAction(string routeKey, string sourceListKey, Action successCallback) - { - CrestronInvoke.BeginInvoke(o => - { - Debug.LogMessage(LogEventLevel.Debug, this, "Run route action '{0}' on SourceList: {1}", routeKey, sourceListKey); - - var dict = ConfigReader.ConfigObject.GetSourceListForKey(sourceListKey); - if (dict == null) - { - Debug.LogMessage(LogEventLevel.Debug, this, "WARNING: Config source list '{0}' not found", sourceListKey); - return; - } - - // Try to get the list item by it's string key - if (!dict.ContainsKey(routeKey)) - { - Debug.LogMessage(LogEventLevel.Debug, this, "WARNING: No item '{0}' found on config list '{1}'", - routeKey, sourceListKey); - return; - } - - var item = dict[routeKey]; - - foreach (var route in item.RouteList) - { - DoRoute(route); - } - - // store the name and UI info for routes - if (item.SourceKey == "none") - { - CurrentSourceInfoKey = routeKey; - CurrentSourceInfo = null; - } - else if (item.SourceKey != null) - { - CurrentSourceInfoKey = routeKey; - CurrentSourceInfo = item; - } - - // report back when done - if (successCallback != null) - successCallback(); - }); - } - - #endregion - - /// - /// - /// - /// - /// - bool DoRoute(SourceRouteListItem route) - { - IRoutingSink dest = null; - - dest = DeviceManager.GetDeviceForKey(route.DestinationKey) as IRoutingSink; - - if (dest == null) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Cannot route, unknown destination '{0}'", route.DestinationKey); - return false; - } - - if (route.SourceKey.Equals("$off", StringComparison.OrdinalIgnoreCase)) - { - dest.ReleaseRoute(); - if (dest is IHasPowerControl) - (dest as IHasPowerControl).PowerOff(); - } - else - { - var source = DeviceManager.GetDeviceForKey(route.SourceKey) as IRoutingOutputs; - if (source == null) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Cannot route unknown source '{0}' to {1}", route.SourceKey, route.DestinationKey); - return false; - } - dest.ReleaseAndMakeRoute(source, route.Type); - } - return true; - } - - - - #region IHasCurrentSourceInfoChange Members - - /// - /// Gets or sets the CurrentSourceInfoKey - /// - public string CurrentSourceInfoKey { get; set; } - - /// - /// The SourceListItem last run - containing names and icons - /// - public SourceListItem CurrentSourceInfo - { - get { return _CurrentSourceInfo; } - set - { - if (value == _CurrentSourceInfo) return; - - var handler = CurrentSourceChange; - // remove from in-use tracker, if so equipped - if (_CurrentSourceInfo != null && _CurrentSourceInfo.SourceDevice is IInUseTracking) - (_CurrentSourceInfo.SourceDevice as IInUseTracking).InUseTracker.RemoveUser(this, "control"); - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.WillChange); - - _CurrentSourceInfo = value; - - // add to in-use tracking - if (_CurrentSourceInfo != null && _CurrentSourceInfo.SourceDevice is IInUseTracking) - (_CurrentSourceInfo.SourceDevice as IInUseTracking).InUseTracker.AddUser(this, "control"); - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.DidChange); - } - } - SourceListItem _CurrentSourceInfo; - - /// - /// Event fired when the current source changes - /// - public event SourceInfoChangeHandler CurrentSourceChange; - - #endregion + (AnyVideoIn = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this)) + }; } -} \ No newline at end of file + #region IRunRouteAction Members + + public void RunRouteAction(string routeKey, string sourceListKey) + { + RunRouteAction(routeKey, sourceListKey, null); + } + + public void RunRouteAction(string routeKey, string sourceListKey, Action successCallback) + { + CrestronInvoke.BeginInvoke(o => + { + Debug.LogMessage(LogEventLevel.Debug, this, "Run route action '{0}' on SourceList: {1}", routeKey, sourceListKey); + + var dict = ConfigReader.ConfigObject.GetSourceListForKey(sourceListKey); + if (dict == null) + { + Debug.LogMessage(LogEventLevel.Debug, this, "WARNING: Config source list '{0}' not found", sourceListKey); + return; + } + + // Try to get the list item by it's string key + if (!dict.ContainsKey(routeKey)) + { + Debug.LogMessage(LogEventLevel.Debug, this, "WARNING: No item '{0}' found on config list '{1}'", + routeKey, sourceListKey); + return; + } + + var item = dict[routeKey]; + + foreach (var route in item.RouteList) + { + DoRoute(route); + } + + // store the name and UI info for routes + if (item.SourceKey == "none") + { + CurrentSourceInfoKey = routeKey; + CurrentSourceInfo = null; + } + else if (item.SourceKey != null) + { + CurrentSourceInfoKey = routeKey; + CurrentSourceInfo = item; + } + + // report back when done + if (successCallback != null) + successCallback(); + }); + } + + #endregion + + /// + /// + /// + /// + /// + bool DoRoute(SourceRouteListItem route) + { + IRoutingSink dest = null; + + dest = DeviceManager.GetDeviceForKey(route.DestinationKey) as IRoutingSink; + + if (dest == null) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Cannot route, unknown destination '{0}'", route.DestinationKey); + return false; + } + + if (route.SourceKey.Equals("$off", StringComparison.OrdinalIgnoreCase)) + { + dest.ReleaseRoute(); + if (dest is IHasPowerControl) + (dest as IHasPowerControl).PowerOff(); + } + else + { + var source = DeviceManager.GetDeviceForKey(route.SourceKey) as IRoutingOutputs; + if (source == null) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Cannot route unknown source '{0}' to {1}", route.SourceKey, route.DestinationKey); + return false; + } + dest.ReleaseAndMakeRoute(source, route.Type); + } + return true; + } + + + + #region IHasCurrentSourceInfoChange Members + + public string CurrentSourceInfoKey { get; set; } + + /// + /// The SourceListItem last run - containing names and icons + /// + public SourceListItem CurrentSourceInfo + { + get { return _CurrentSourceInfo; } + set + { + if (value == _CurrentSourceInfo) return; + + var handler = CurrentSourceChange; + // remove from in-use tracker, if so equipped + if (_CurrentSourceInfo != null && _CurrentSourceInfo.SourceDevice is IInUseTracking) + (_CurrentSourceInfo.SourceDevice as IInUseTracking).InUseTracker.RemoveUser(this, "control"); + + if (handler != null) + handler(_CurrentSourceInfo, ChangeType.WillChange); + + _CurrentSourceInfo = value; + + // add to in-use tracking + if (_CurrentSourceInfo != null && _CurrentSourceInfo.SourceDevice is IInUseTracking) + (_CurrentSourceInfo.SourceDevice as IInUseTracking).InUseTracker.AddUser(this, "control"); + if (handler != null) + handler(_CurrentSourceInfo, ChangeType.DidChange); + } + } + SourceListItem _CurrentSourceInfo; + + public event SourceInfoChangeHandler CurrentSourceChange; + + #endregion +} diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs index f8a8f640..942832f3 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs @@ -3,132 +3,130 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.SoftCodec +namespace PepperDash.Essentials.Devices.Common.SoftCodec; + + +/// +/// Represents a GenericSoftCodec +/// +public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWithSwitchingWithInputPort { + private RoutingInputPort _currentInputPort; + /// - /// Represents a GenericSoftCodec + /// Gets or sets the CurrentInputPort /// - public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWithSwitchingWithInputPort + public RoutingInputPort CurrentInputPort { - private RoutingInputPort _currentInputPort; - - /// - /// Gets or sets the CurrentInputPort - /// - public RoutingInputPort CurrentInputPort + get => _currentInputPort; + set { - get => _currentInputPort; - set - { - _currentInputPort = value; + _currentInputPort = value; - InputChanged?.Invoke(this, _currentInputPort); - } - } - - /// - /// Initializes a new instance of the class - /// - /// The device key - /// The device name - /// The device properties - public GenericSoftCodec(string key, string name, GenericSoftCodecProperties props) : base(key, name) - { - InputPorts = new RoutingPortCollection(); - OutputPorts = new RoutingPortCollection(); - - for (var i = 1; i <= props.OutputCount; i++) - { - var outputPort = new RoutingOutputPort($"output{i}", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); - - OutputPorts.Add(outputPort); - } - - for (var i = 1; i <= props.ContentInputCount; i++) - { - var inputPort = new RoutingInputPort($"contentInput{i}", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, $"contentInput{i}", this); - - InputPorts.Add(inputPort); - } - - if (!props.HasCameraInputs) - { - return; - } - - for (var i = 1; i <= props.CameraInputCount; i++) - { - var cameraPort = new RoutingInputPort($"cameraInput{i}", eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, $"cameraInput{i}", this); - - InputPorts.Add(cameraPort); - } - } - - /// - /// Gets or sets the InputPorts - /// - public RoutingPortCollection InputPorts { get; private set; } - - /// - /// Gets or sets the OutputPorts - /// - public RoutingPortCollection OutputPorts { get; private set; } - /// - /// Gets or sets the CurrentSourceInfoKey - /// - public string CurrentSourceInfoKey { get; set; } - - /// - /// Gets or sets the CurrentSourceInfo - /// - public SourceListItem CurrentSourceInfo - { - get - { - return _CurrentSourceInfo; - } - set - { - if (value == _CurrentSourceInfo) return; - - var handler = CurrentSourceChange; - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.WillChange); - - _CurrentSourceInfo = value; - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.DidChange); - } - } - - SourceListItem _CurrentSourceInfo; - - /// - /// Event fired when the current source changes - /// - public event SourceInfoChangeHandler CurrentSourceChange; - - /// - /// Event fired when the input changes - /// - public event InputChangedEventHandler InputChanged; - - /// - /// ExecuteSwitch method - /// - public void ExecuteSwitch(object inputSelector) - { - var inputPort = InputPorts.FirstOrDefault(p => p.Selector == inputSelector); - - if (inputPort == null) - { - Debug.LogMessage(LogEventLevel.Warning, "No input port found for selector {inputSelector}", inputSelector); - return; - } - - CurrentInputPort = inputPort; + InputChanged?.Invoke(this, _currentInputPort); } } + + /// + /// Initializes a new instance of the class + /// + /// The device key + /// The device name + /// The device properties + public GenericSoftCodec(string key, string name, GenericSoftCodecProperties props) : base(key, name) + { + for (var i = 1; i <= props.OutputCount; i++) + { + var outputPort = new RoutingOutputPort($"output{i}", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); + + OutputPorts.Add(outputPort); + } + + for (var i = 1; i <= props.ContentInputCount; i++) + { + var inputPort = new RoutingInputPort($"contentInput{i}", eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, $"contentInput{i}", this); + + InputPorts.Add(inputPort); + } + + if (!props.HasCameraInputs) + { + return; + } + + for (var i = 1; i <= props.CameraInputCount; i++) + { + var cameraPort = new RoutingInputPort($"cameraInput{i}", eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, $"cameraInput{i}", this); + + InputPorts.Add(cameraPort); + } + } + + /// + /// Gets or sets the InputPorts + /// + public RoutingPortCollection InputPorts { get; private set; } + + /// + /// Gets or sets the OutputPorts + /// + public RoutingPortCollection OutputPorts { get; private set; } + /// + /// Gets or sets the CurrentSourceInfoKey + /// + public string CurrentSourceInfoKey { get; set; } + + /// + /// Gets or sets the CurrentSourceInfo + /// + public SourceListItem CurrentSourceInfo + { + get + { + return _CurrentSourceInfo; + } + set + { + if (value == _CurrentSourceInfo) return; + + var handler = CurrentSourceChange; + + if (handler != null) + handler(_CurrentSourceInfo, ChangeType.WillChange); + + _CurrentSourceInfo = value; + + if (handler != null) + handler(_CurrentSourceInfo, ChangeType.DidChange); + } + } + + SourceListItem _CurrentSourceInfo; + + /// + /// Event fired when the current source changes + /// + public event SourceInfoChangeHandler CurrentSourceChange; + + /// + /// Event fired when the input changes + /// + public event InputChangedEventHandler InputChanged; + + /// + /// ExecuteSwitch method + /// + public void ExecuteSwitch(object inputSelector) + { + var inputPort = InputPorts.FirstOrDefault(p => p.Selector == inputSelector); + + if (inputPort == null) + { + Debug.LogMessage(LogEventLevel.Warning, "No input port found for selector {inputSelector}", inputSelector); + return; + } + + CurrentInputPort = inputPort; + } } + diff --git a/src/PepperDash.Essentials.Devices.Common/Sources/InRoomPc.cs b/src/PepperDash.Essentials.Devices.Common/Sources/InRoomPc.cs index e2ef1c09..d12dd37a 100644 --- a/src/PepperDash.Essentials.Devices.Common/Sources/InRoomPc.cs +++ b/src/PepperDash.Essentials.Devices.Common/Sources/InRoomPc.cs @@ -1,83 +1,64 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.Sources +namespace PepperDash.Essentials.Devices.Common.Sources; + +public class InRoomPc : EssentialsDevice, IHasFeedback, IRoutingSource, IRoutingOutputs, IAttachVideoStatus, IUiDisplayInfo, IUsageTracking { + public uint DisplayUiType { get { return DisplayUiConstants.TypeLaptop; } } + public string IconName { get; set; } + public BoolFeedback HasPowerOnFeedback { get; private set; } + /// - /// Represents a InRoomPc + /// Gets or sets the AnyVideoOut /// - public class InRoomPc : EssentialsDevice, IHasFeedback, IRoutingSource, IRoutingOutputs, IAttachVideoStatus, IUiDisplayInfo, IUsageTracking + public RoutingOutputPort AnyVideoOut { get; private set; } + + #region IRoutingOutputs Members + + /// + /// Gets or sets the OutputPorts + /// + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + /// + /// Initializes a new instance of the class + /// + /// + /// + public InRoomPc(string key, string name) + : base(key, name) { - /// - /// Gets or sets the DisplayUiType - /// - public uint DisplayUiType { get { return DisplayUiConstants.TypeLaptop; } } - /// - /// Gets or sets the IconName - /// - public string IconName { get; set; } - /// - /// Gets or sets the HasPowerOnFeedback - /// - public BoolFeedback HasPowerOnFeedback { get; private set; } + IconName = "PC"; + HasPowerOnFeedback = new BoolFeedback("HasPowerFeedback", + () => this.GetVideoStatuses() != VideoStatusOutputs.NoStatus); - /// - /// Gets or sets the AnyVideoOut - /// - public RoutingOutputPort AnyVideoOut { get; private set; } - - #region IRoutingOutputs Members - - /// - /// Gets or sets the OutputPorts - /// - public RoutingPortCollection OutputPorts { get; private set; } - - #endregion - - /// - /// Initializes a new instance of the class - /// - /// - /// - public InRoomPc(string key, string name) - : base(key, name) - { - IconName = "PC"; - HasPowerOnFeedback = new BoolFeedback("HasPowerFeedback", - () => this.GetVideoStatuses() != VideoStatusOutputs.NoStatus); - - OutputPorts = new RoutingPortCollection + OutputPorts = new RoutingPortCollection { (AnyVideoOut = new RoutingOutputPort(RoutingPortNames.AnyVideoOut, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.None, 0, this)) }; - } - - #region IHasFeedback Members - - /// - /// Passes through the VideoStatuses list - /// - public FeedbackCollection Feedbacks - { - get - { - var newList = new FeedbackCollection(); - newList.AddRange(this.GetVideoStatuses().ToList()); - return newList; - } - } - - #endregion - - #region IUsageTracking Members - - /// - /// Gets or sets the UsageTracker - /// - public UsageTracking UsageTracker { get; set; } - - #endregion } -} \ No newline at end of file + /// + /// Passes through the VideoStatuses list + /// + public FeedbackCollection Feedbacks + { + get + { + var newList = new FeedbackCollection(); + newList.AddRange(this.GetVideoStatuses().ToList()); + return newList; + } + } + + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion +} + diff --git a/src/PepperDash.Essentials.Devices.Common/Sources/Laptop.cs b/src/PepperDash.Essentials.Devices.Common/Sources/Laptop.cs index 83ccfc4c..0a7466e7 100644 --- a/src/PepperDash.Essentials.Devices.Common/Sources/Laptop.cs +++ b/src/PepperDash.Essentials.Devices.Common/Sources/Laptop.cs @@ -1,83 +1,68 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.Sources +namespace PepperDash.Essentials.Devices.Common.Sources; + +public class Laptop : EssentialsDevice, IHasFeedback, IRoutingSource, IRoutingOutputs, IAttachVideoStatus, IUiDisplayInfo, IUsageTracking { + public uint DisplayUiType { get { return DisplayUiConstants.TypeLaptop; } } + public string IconName { get; set; } + public BoolFeedback HasPowerOnFeedback { get; private set; } + /// - /// Represents a Laptop + /// Gets or sets the AnyVideoOut /// - public class Laptop : EssentialsDevice, IHasFeedback, IRoutingSource, IRoutingOutputs, IAttachVideoStatus, IUiDisplayInfo, IUsageTracking + public RoutingOutputPort AnyVideoOut { get; private set; } + + #region IRoutingOutputs Members + + /// + /// Gets or sets the OutputPorts + /// + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + /// + /// Initializes a new instance of the Laptop class + /// + /// The device key + /// The device name + public Laptop(string key, string name) + : base(key, name) { - /// - /// Gets or sets the DisplayUiType - /// - public uint DisplayUiType { get { return DisplayUiConstants.TypeLaptop; } } - /// - /// Gets or sets the IconName - /// - public string IconName { get; set; } - /// - /// Gets or sets the HasPowerOnFeedback - /// - public BoolFeedback HasPowerOnFeedback { get; private set; } + IconName = "Laptop"; - /// - /// Gets or sets the AnyVideoOut - /// - public RoutingOutputPort AnyVideoOut { get; private set; } + HasPowerOnFeedback = new BoolFeedback("HasPowerFeedback", + () => this.GetVideoStatuses() != VideoStatusOutputs.NoStatus); - #region IRoutingOutputs Members - - /// - /// Gets or sets the OutputPorts - /// - public RoutingPortCollection OutputPorts { get; private set; } - - #endregion - - /// - /// Initializes a new instance of the Laptop class - /// - /// The device key - /// The device name - public Laptop(string key, string name) - : base(key, name) + OutputPorts = new RoutingPortCollection { - IconName = "Laptop"; - - HasPowerOnFeedback = new BoolFeedback("HasPowerFeedback", - () => this.GetVideoStatuses() != VideoStatusOutputs.NoStatus); - - OutputPorts = new RoutingPortCollection - { - (AnyVideoOut = new RoutingOutputPort(RoutingPortNames.AnyOut, eRoutingSignalType.Audio | eRoutingSignalType.Video, - eRoutingPortConnectionType.None, 0, this)) - }; - } - - #region IHasFeedback Members - - /// - /// Passes through the VideoStatuses list - /// - public FeedbackCollection Feedbacks - { - get - { - var newList = new FeedbackCollection(); - newList.AddRange(this.GetVideoStatuses().ToList()); - return newList; - } - } - - #endregion - - #region IUsageTracking Members - - /// - /// Gets or sets the UsageTracker - /// - public UsageTracking UsageTracker { get; set; } - - #endregion + (AnyVideoOut = new RoutingOutputPort(RoutingPortNames.AnyOut, eRoutingSignalType.Audio | eRoutingSignalType.Video, + eRoutingPortConnectionType.None, 0, this)) + }; } -} \ No newline at end of file + + #region IHasFeedback Members + + /// + /// Passes through the VideoStatuses list + /// + public FeedbackCollection Feedbacks + { + get + { + var newList = new FeedbackCollection(); + newList.AddRange(this.GetVideoStatuses().ToList()); + return newList; + } + } + + #endregion + + #region IUsageTracking Members + + public UsageTracking UsageTracker { get; set; } + + #endregion +} + diff --git a/src/PepperDash.Essentials.Devices.Common/Streaming/AppleTV.cs b/src/PepperDash.Essentials.Devices.Common/Streaming/AppleTV.cs index 6cf11c54..453b6aa0 100644 --- a/src/PepperDash.Essentials.Devices.Common/Streaming/AppleTV.cs +++ b/src/PepperDash.Essentials.Devices.Common/Streaming/AppleTV.cs @@ -7,244 +7,244 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common +namespace PepperDash.Essentials.Devices.Common; + +/// +/// Represents a AppleTV +/// Wrapper class for an IR-Controlled AppleTV +/// +[Description("Wrapper class for an IR-Controlled AppleTV")] +public class AppleTV : EssentialsBridgeableDevice, IDPad, ITransport, IUiDisplayInfo, IRoutingSource, IRoutingOutputs { /// - /// Represents a AppleTV - /// Wrapper class for an IR-Controlled AppleTV + /// Gets or sets the IrPort /// - [Description("Wrapper class for an IR-Controlled AppleTV")] - public class AppleTV : EssentialsBridgeableDevice, IDPad, ITransport, IUiDisplayInfo, IRoutingSource, IRoutingOutputs + public IrOutputPortController IrPort { get; private set; } + /// + /// Standard Driver Name + /// + public const string StandardDriverName = "Apple_AppleTV_4th_Gen_Essentials.ir"; + /// + /// Gets or sets the DisplayUiType + /// + public uint DisplayUiType { get { return DisplayUiConstants.TypeAppleTv; } } + + /// + /// Initializes a new instance of the class + /// + /// The device key + /// The device name + /// The IR output port controller + public AppleTV(string key, string name, IrOutputPortController portCont) + : base(key, name) { - /// - /// Gets or sets the IrPort - /// - public IrOutputPortController IrPort { get; private set; } + IrPort = portCont; + DeviceManager.AddDevice(portCont); - /// - /// Standard Driver Name - /// - public const string StandardDriverName = "Apple_AppleTV_4th_Gen_Essentials.ir"; - /// - /// Gets or sets the DisplayUiType - /// - public uint DisplayUiType { get { return DisplayUiConstants.TypeAppleTv; } } + HdmiOut = new RoutingOutputPort(RoutingPortNames.HdmiOut, eRoutingSignalType.Audio | eRoutingSignalType.Video, + eRoutingPortConnectionType.Hdmi, null, this); + AnyAudioOut = new RoutingOutputPort(RoutingPortNames.AnyAudioOut, eRoutingSignalType.Audio, + eRoutingPortConnectionType.DigitalAudio, null, this); + OutputPorts = new RoutingPortCollection { HdmiOut, AnyAudioOut }; - /// - /// Initializes a new instance of the class - /// - /// The device key - /// The device name - /// The IR output port controller - public AppleTV(string key, string name, IrOutputPortController portCont) - : base(key, name) + PrintExpectedIrCommands(); + } + + /// + /// PrintExpectedIrCommands method + /// + public void PrintExpectedIrCommands() + { + var cmds = typeof(AppleTvIrCommands).GetFields(BindingFlags.Public | BindingFlags.Static); + + foreach (var value in cmds.Select(cmd => cmd.GetValue(null)).OfType()) { - IrPort = portCont; - DeviceManager.AddDevice(portCont); - - HdmiOut = new RoutingOutputPort(RoutingPortNames.HdmiOut, eRoutingSignalType.Audio | eRoutingSignalType.Video, - eRoutingPortConnectionType.Hdmi, null, this); - AnyAudioOut = new RoutingOutputPort(RoutingPortNames.AnyAudioOut, eRoutingSignalType.Audio, - eRoutingPortConnectionType.DigitalAudio, null, this); - OutputPorts = new RoutingPortCollection { HdmiOut, AnyAudioOut }; - - PrintExpectedIrCommands(); - } - - /// - /// PrintExpectedIrCommands method - /// - public void PrintExpectedIrCommands() - { - var cmds = typeof(AppleTvIrCommands).GetFields(BindingFlags.Public | BindingFlags.Static); - - foreach (var value in cmds.Select(cmd => cmd.GetValue(null)).OfType()) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Expected IR Function Name: {0}", value); - } - } - - #region IDPad Members - - /// - /// Up method - /// - public void Up(bool pressRelease) - { - IrPort.PressRelease(AppleTvIrCommands.Up, pressRelease); - } - - /// - /// Down method - /// - public void Down(bool pressRelease) - { - IrPort.PressRelease(AppleTvIrCommands.Down, pressRelease); - } - - /// - /// Left method - /// - public void Left(bool pressRelease) - { - IrPort.PressRelease(AppleTvIrCommands.Left, pressRelease); - } - - /// - /// Right method - /// - public void Right(bool pressRelease) - { - IrPort.PressRelease(AppleTvIrCommands.Right, pressRelease); - } - - /// - /// Select method - /// - public void Select(bool pressRelease) - { - IrPort.PressRelease(AppleTvIrCommands.Enter, pressRelease); - } - - /// - /// Menu method - /// - public void Menu(bool pressRelease) - { - IrPort.PressRelease(AppleTvIrCommands.Menu, pressRelease); - } - - /// - /// Exit method - /// - public void Exit(bool pressRelease) - { - - } - - #endregion - - #region ITransport Members - - /// - /// Play method - /// - public void Play(bool pressRelease) - { - IrPort.PressRelease(AppleTvIrCommands.PlayPause, pressRelease); - } - - /// - /// Pause method - /// - public void Pause(bool pressRelease) - { - IrPort.PressRelease(AppleTvIrCommands.PlayPause, pressRelease); - } - - /// - /// Not implemented - /// - /// - /// - /// Rewind method - /// - public void Rewind(bool pressRelease) - { - } - - /// - /// Not implemented - /// - /// - public void FFwd(bool pressRelease) - { - } - - /// - /// Not implemented - /// - /// - public void ChapMinus(bool pressRelease) - { - } - - /// - /// Not implemented - /// - /// - public void ChapPlus(bool pressRelease) - { - } - - /// - /// Not implemented - /// - /// - public void Stop(bool pressRelease) - { - } - - /// - /// Not implemented - /// - /// - public void Record(bool pressRelease) - { - } - - #endregion - - #region IRoutingOutputs Members - - /// - /// Gets the HdmiOut - /// - public RoutingOutputPort HdmiOut { get; private set; } - - /// - /// Gets the AnyAudioOut - /// - public RoutingOutputPort AnyAudioOut { get; private set; } - /// - /// Gets or sets the OutputPorts - /// - public RoutingPortCollection OutputPorts { get; private set; } - - #endregion - - /// - /// LinkToApi method - /// - public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - var joinMap = new AppleTvJoinMap(joinStart); - - var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); - - if (!string.IsNullOrEmpty(joinMapSerialized)) - joinMap = JsonConvert.DeserializeObject(joinMapSerialized); - - if (bridge != null) - { - bridge.AddJoinMap(Key, joinMap); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); - } - - Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); - Debug.LogMessage(LogEventLevel.Information, "Linking to Bridge Type {0}", GetType().Name); - - trilist.SetBoolSigAction(joinMap.UpArrow.JoinNumber, Up); - trilist.SetBoolSigAction(joinMap.DnArrow.JoinNumber, Down); - trilist.SetBoolSigAction(joinMap.LeftArrow.JoinNumber, Left); - trilist.SetBoolSigAction(joinMap.RightArrow.JoinNumber, Right); - trilist.SetBoolSigAction(joinMap.Select.JoinNumber, Select); - trilist.SetBoolSigAction(joinMap.Menu.JoinNumber, Menu); - trilist.SetBoolSigAction(joinMap.PlayPause.JoinNumber, Play); + Debug.LogMessage(LogEventLevel.Verbose, this, "Expected IR Function Name: {0}", value); } } -} \ No newline at end of file + + + #region IDPad Members + + /// + /// Up method + /// + public void Up(bool pressRelease) + { + IrPort.PressRelease(AppleTvIrCommands.Up, pressRelease); + } + + /// + /// Down method + /// + public void Down(bool pressRelease) + { + IrPort.PressRelease(AppleTvIrCommands.Down, pressRelease); + } + + /// + /// Left method + /// + public void Left(bool pressRelease) + { + IrPort.PressRelease(AppleTvIrCommands.Left, pressRelease); + } + + /// + /// Right method + /// + public void Right(bool pressRelease) + { + IrPort.PressRelease(AppleTvIrCommands.Right, pressRelease); + } + + /// + /// Select method + /// + public void Select(bool pressRelease) + { + IrPort.PressRelease(AppleTvIrCommands.Enter, pressRelease); + } + + /// + /// Menu method + /// + public void Menu(bool pressRelease) + { + IrPort.PressRelease(AppleTvIrCommands.Menu, pressRelease); + } + + /// + /// Exit method + /// + public void Exit(bool pressRelease) + { + + } + + #endregion + + #region ITransport Members + + /// + /// Play method + /// + public void Play(bool pressRelease) + { + IrPort.PressRelease(AppleTvIrCommands.PlayPause, pressRelease); + } + + /// + /// Pause method + /// + public void Pause(bool pressRelease) + { + IrPort.PressRelease(AppleTvIrCommands.PlayPause, pressRelease); + } + + /// + /// Not implemented + /// + /// + /// + /// Rewind method + /// + public void Rewind(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void FFwd(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void ChapMinus(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void ChapPlus(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void Stop(bool pressRelease) + { + } + + /// + /// Not implemented + /// + /// + public void Record(bool pressRelease) + { + } + + #endregion + + #region IRoutingOutputs Members + + /// + /// Gets the HdmiOut + /// + public RoutingOutputPort HdmiOut { get; private set; } + + /// + /// Gets the AnyAudioOut + /// + public RoutingOutputPort AnyAudioOut { get; private set; } + /// + /// Gets or sets the OutputPorts + /// + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + /// + /// LinkToApi method + /// + public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + var joinMap = new AppleTvJoinMap(joinStart); + + var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey); + + if (!string.IsNullOrEmpty(joinMapSerialized)) + joinMap = JsonConvert.DeserializeObject(joinMapSerialized); + + if (bridge != null) + { + bridge.AddJoinMap(Key, joinMap); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device."); + } + + Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X")); + Debug.LogMessage(LogEventLevel.Information, "Linking to Bridge Type {0}", GetType().Name); + + trilist.SetBoolSigAction(joinMap.UpArrow.JoinNumber, Up); + trilist.SetBoolSigAction(joinMap.DnArrow.JoinNumber, Down); + trilist.SetBoolSigAction(joinMap.LeftArrow.JoinNumber, Left); + trilist.SetBoolSigAction(joinMap.RightArrow.JoinNumber, Right); + trilist.SetBoolSigAction(joinMap.Select.JoinNumber, Select); + trilist.SetBoolSigAction(joinMap.Menu.JoinNumber, Menu); + trilist.SetBoolSigAction(joinMap.PlayPause.JoinNumber, Play); + } +} + diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/CallHistoryDataClasses.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/CallHistoryDataClasses.cs index 8872172a..8f4f1b5b 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/CallHistoryDataClasses.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/CallHistoryDataClasses.cs @@ -1,220 +1,220 @@ using System; using System.Collections.Generic; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + + +/// +/// Represents a CiscoCallHistory +/// +public class CiscoCallHistory { /// - /// Represents a CiscoCallHistory + /// Represents a CallbackNumber /// - public class CiscoCallHistory + public class CallbackNumber { /// - /// Represents a CallbackNumber + /// Gets or sets the Value /// - public class CallbackNumber - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a DisplayName - /// - public class DisplayName - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a LastOccurrenceStartTime - /// - public class LastOccurrenceStartTime - { - /// - /// Gets or sets the Value - /// - public DateTime Value { get; set; } - } - - /// - /// Represents a LastOccurrenceDaysAgo - /// - public class LastOccurrenceDaysAgo - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a LastOccurrenceHistoryId - /// - public class LastOccurrenceHistoryId - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a OccurrenceType - /// - public class OccurrenceType - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a IsAcknowledged - /// - public class IsAcknowledged - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a OccurrenceCount - /// - public class OccurrenceCount - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a Entry - /// - public class Entry - { - /// - /// Gets or sets the id - /// - public string id { get; set; } - /// - /// Gets or sets the CallbackNumber - /// - public CallbackNumber CallbackNumber { get; set; } - /// - /// Gets or sets the DisplayName - /// - public DisplayName DisplayName { get; set; } - /// - /// Gets or sets the LastOccurrenceStartTime - /// - public LastOccurrenceStartTime LastOccurrenceStartTime { get; set; } - /// - /// Gets or sets the LastOccurrenceDaysAgo - /// - public LastOccurrenceDaysAgo LastOccurrenceDaysAgo { get; set; } - /// - /// Gets or sets the LastOccurrenceHistoryId - /// - public LastOccurrenceHistoryId LastOccurrenceHistoryId { get; set; } - /// - /// Gets or sets the OccurrenceType - /// - public OccurrenceType OccurrenceType { get; set; } - /// - /// Gets or sets the IsAcknowledged - /// - public IsAcknowledged IsAcknowledged { get; set; } - /// - /// Gets or sets the OccurrenceCount - /// - public OccurrenceCount OccurrenceCount { get; set; } - } - - /// - /// Represents a Offset - /// - public class Offset - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a Limit - /// - public class Limit - { - /// - /// Gets or sets the Value - /// - public string Value { get; set; } - } - - /// - /// Represents a ResultInfo - /// - public class ResultInfo - { - /// - /// Gets or sets the Offset - /// - public Offset Offset { get; set; } - /// - /// Gets or sets the Limit - /// - public Limit Limit { get; set; } - } - - /// - /// Represents a CallHistoryRecentsResult - /// - public class CallHistoryRecentsResult - { - /// - /// Gets or sets the status - /// - public string status { get; set; } - /// - /// Gets or sets the Entry - /// - public List Entry { get; set; } - /// - /// Gets or sets the ResultInfo - /// - public ResultInfo ResultInfo { get; set; } - } - - /// - /// Represents a CommandResponse - /// - public class CommandResponse - { - /// - /// Gets or sets the CallHistoryRecentsResult - /// - public CallHistoryRecentsResult CallHistoryRecentsResult { get; set; } - } - - /// - /// Represents a RootObject - /// - public class RootObject - { - /// - /// Gets or sets the CommandResponse - /// - public CommandResponse CommandResponse { get; set; } - } + public string Value { get; set; } } -} \ No newline at end of file + + /// + /// Represents a DisplayName + /// + public class DisplayName + { + /// + /// Gets or sets the Value + /// + public string Value { get; set; } + } + + /// + /// Represents a LastOccurrenceStartTime + /// + public class LastOccurrenceStartTime + { + /// + /// Gets or sets the Value + /// + public DateTime Value { get; set; } + } + + /// + /// Represents a LastOccurrenceDaysAgo + /// + public class LastOccurrenceDaysAgo + { + /// + /// Gets or sets the Value + /// + public string Value { get; set; } + } + + /// + /// Represents a LastOccurrenceHistoryId + /// + public class LastOccurrenceHistoryId + { + /// + /// Gets or sets the Value + /// + public string Value { get; set; } + } + + /// + /// Represents a OccurrenceType + /// + public class OccurrenceType + { + /// + /// Gets or sets the Value + /// + public string Value { get; set; } + } + + /// + /// Represents a IsAcknowledged + /// + public class IsAcknowledged + { + /// + /// Gets or sets the Value + /// + public string Value { get; set; } + } + + /// + /// Represents a OccurrenceCount + /// + public class OccurrenceCount + { + /// + /// Gets or sets the Value + /// + public string Value { get; set; } + } + + /// + /// Represents a Entry + /// + public class Entry + { + /// + /// Gets or sets the id + /// + public string id { get; set; } + /// + /// Gets or sets the CallbackNumber + /// + public CallbackNumber CallbackNumber { get; set; } + /// + /// Gets or sets the DisplayName + /// + public DisplayName DisplayName { get; set; } + /// + /// Gets or sets the LastOccurrenceStartTime + /// + public LastOccurrenceStartTime LastOccurrenceStartTime { get; set; } + /// + /// Gets or sets the LastOccurrenceDaysAgo + /// + public LastOccurrenceDaysAgo LastOccurrenceDaysAgo { get; set; } + /// + /// Gets or sets the LastOccurrenceHistoryId + /// + public LastOccurrenceHistoryId LastOccurrenceHistoryId { get; set; } + /// + /// Gets or sets the OccurrenceType + /// + public OccurrenceType OccurrenceType { get; set; } + /// + /// Gets or sets the IsAcknowledged + /// + public IsAcknowledged IsAcknowledged { get; set; } + /// + /// Gets or sets the OccurrenceCount + /// + public OccurrenceCount OccurrenceCount { get; set; } + } + + /// + /// Represents a Offset + /// + public class Offset + { + /// + /// Gets or sets the Value + /// + public string Value { get; set; } + } + + /// + /// Represents a Limit + /// + public class Limit + { + /// + /// Gets or sets the Value + /// + public string Value { get; set; } + } + + /// + /// Represents a ResultInfo + /// + public class ResultInfo + { + /// + /// Gets or sets the Offset + /// + public Offset Offset { get; set; } + /// + /// Gets or sets the Limit + /// + public Limit Limit { get; set; } + } + + /// + /// Represents a CallHistoryRecentsResult + /// + public class CallHistoryRecentsResult + { + /// + /// Gets or sets the status + /// + public string status { get; set; } + /// + /// Gets or sets the Entry + /// + public List Entry { get; set; } + /// + /// Gets or sets the ResultInfo + /// + public ResultInfo ResultInfo { get; set; } + } + + /// + /// Represents a CommandResponse + /// + public class CommandResponse + { + /// + /// Gets or sets the CallHistoryRecentsResult + /// + public CallHistoryRecentsResult CallHistoryRecentsResult { get; set; } + } + + /// + /// Represents a RootObject + /// + public class RootObject + { + /// + /// Gets or sets the CommandResponse + /// + public CommandResponse CommandResponse { get; set; } + } +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/RoomPresets.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/RoomPresets.cs index e6e2be7e..333c4aa9 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/RoomPresets.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/RoomPresets.cs @@ -3,84 +3,83 @@ using System.Collections.Generic; using System.Linq; using PepperDash.Essentials.Core.Presets; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + +/// +/// Interface for camera presets +/// +public interface IHasCodecRoomPresets { /// - /// Interface for camera presets + /// Event that is raised when the list of room presets has changed. /// - public interface IHasCodecRoomPresets - { - /// - /// Event that is raised when the list of room presets has changed. - /// - event EventHandler CodecRoomPresetsListHasChanged; - - /// - /// List of near end presets that can be recalled. - /// - List NearEndPresets { get; } - - /// - /// List of far end presets that can be recalled. - /// - List FarEndRoomPresets { get; } - - /// - /// Selects a near end preset by its ID. - /// - /// - void CodecRoomPresetSelect(int preset); - - /// - /// Stores a near end preset with the given ID and description. - /// - /// - /// - void CodecRoomPresetStore(int preset, string description); - - /// - /// Selects a far end preset by its ID. This is typically used to recall a preset that has been defined on the far end codec. - /// - /// - void SelectFarEndPreset(int preset); - } + event EventHandler CodecRoomPresetsListHasChanged; /// - /// Static class for converting non-generic RoomPresets to generic CameraPresets. + /// List of near end presets that can be recalled. /// - public static class RoomPresets - { - /// - /// Converts non-generic RoomPresets to generic CameraPresets - /// - /// - /// - public static List GetGenericPresets(this List presets) where TSource : ConvertiblePreset where TDestination : PresetBase - { - return - presets.Select(preset => preset.ConvertCodecPreset()) - .Where(newPreset => newPreset != null) - .Cast() - .ToList(); - } - } + List NearEndPresets { get; } /// - /// Represents a CodecRoomPreset + /// List of far end presets that can be recalled. /// - public class CodecRoomPreset : PresetBase - { - /// - /// - /// - /// - /// - /// - /// - public CodecRoomPreset(int id, string description, bool def, bool isDef) - : base(id, description, def, isDef) - { + List FarEndRoomPresets { get; } + + /// + /// Selects a near end preset by its ID. + /// + /// + void CodecRoomPresetSelect(int preset); + + /// + /// Stores a near end preset with the given ID and description. + /// + /// + /// + void CodecRoomPresetStore(int preset, string description); + + /// + /// Selects a far end preset by its ID. This is typically used to recall a preset that has been defined on the far end codec. + /// + /// + void SelectFarEndPreset(int preset); +} + +/// +/// Static class for converting non-generic RoomPresets to generic CameraPresets. +/// +public static class RoomPresets +{ + /// + /// Converts non-generic RoomPresets to generic CameraPresets + /// + /// + /// + public static List GetGenericPresets(this List presets) where TSource : ConvertiblePreset where TDestination : PresetBase + { + return + presets.Select(preset => preset.ConvertCodecPreset()) + .Where(newPreset => newPreset != null) + .Cast() + .ToList(); + } +} + +/// +/// Represents a room preset on a video codec. Typically stores camera position(s) and video routing. Can be recalled by Far End if enabled. +/// +public class CodecRoomPreset : PresetBase +{ + /// + /// + /// + /// + /// + /// + /// + public CodecRoomPreset(int id, string description, bool def, bool isDef) + : base(id, description, def, isDef) + { - } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eCommandType.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eCommandType.cs index b68f280d..d33ec5c5 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eCommandType.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eCommandType.cs @@ -1,4 +1,3 @@ -namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco -{ - enum eCommandType { SessionStart, SessionEnd, Command, GetStatus, GetConfiguration }; -} \ No newline at end of file +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Cisco; + +enum eCommandType { SessionStart, SessionEnd, Command, GetStatus, GetConfiguration }; \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eExternalSourceMode.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eExternalSourceMode.cs index 6d77551d..446e4138 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eExternalSourceMode.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eExternalSourceMode.cs @@ -22,4 +22,4 @@ /// Error } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eExternalSourceType.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eExternalSourceType.cs index 6d5811e3..bfee4fed 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eExternalSourceType.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/CiscoCodec/eExternalSourceType.cs @@ -34,4 +34,4 @@ /// other } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/ConvertiblePreset.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/ConvertiblePreset.cs index e75f436d..7298cfe7 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/ConvertiblePreset.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/ConvertiblePreset.cs @@ -1,16 +1,16 @@ using PepperDash.Essentials.Core.Presets; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + + +/// +/// Base class for presets that can be converted to PresetBase +/// +public abstract class ConvertiblePreset { /// - /// Base class for presets that can be converted to PresetBase + /// Converts the preset to a PresetBase /// - public abstract class ConvertiblePreset - { - /// - /// Converts the preset to a PresetBase - /// - /// - public abstract PresetBase ConvertCodecPreset(); - } -} \ No newline at end of file + /// + public abstract PresetBase ConvertCodecPreset(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasCodecLayouts.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasCodecLayouts.cs index 61a40048..bc4e5a80 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasCodecLayouts.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasCodecLayouts.cs @@ -1,30 +1,29 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + +/// +/// Defines the required elements for layout control +/// +public interface IHasCodecLayouts { /// - /// Defines the required elements for layout control + /// Feedback that indicates the current layout on the local display /// - public interface IHasCodecLayouts - { - /// - /// Feedback that indicates the current layout on the local display - /// - StringFeedback LocalLayoutFeedback { get; } + StringFeedback LocalLayoutFeedback { get; } - /// - /// Toggles the local layout - /// - void LocalLayoutToggle(); + /// + /// Toggles the local layout + /// + void LocalLayoutToggle(); - /// - /// Toggles the local layout to single prominent - /// - void LocalLayoutToggleSingleProminent(); + /// + /// Toggles the local layout to single prominent + /// + void LocalLayoutToggleSingleProminent(); - /// - /// Toggle the MinMax layout - /// - void MinMaxLayoutToggle(); - } -} \ No newline at end of file + /// + /// Toggle the MinMax layout + /// + void MinMaxLayoutToggle(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasCodecSelfview.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasCodecSelfview.cs index a52670bc..80617044 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasCodecSelfview.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasCodecSelfview.cs @@ -1,35 +1,35 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + + +/// +/// Defines the requred elements for selfview control +/// +public interface IHasCodecSelfView { /// - /// Defines the requred elements for selfview control + /// Feedback that indicates whether Selfview is on /// - public interface IHasCodecSelfView - { - /// - /// Feedback that indicates whether Selfview is on - /// - BoolFeedback SelfviewIsOnFeedback { get; } + BoolFeedback SelfviewIsOnFeedback { get; } - /// - /// Setting that indicates whether the device shows selfview by default - /// - bool ShowSelfViewByDefault { get; } + /// + /// Setting that indicates whether the device shows selfview by default + /// + bool ShowSelfViewByDefault { get; } - /// - /// Turns selfview on - /// - void SelfViewModeOn(); + /// + /// Turns selfview on + /// + void SelfViewModeOn(); - /// - /// Turns selfview off - /// - void SelfViewModeOff(); + /// + /// Turns selfview off + /// + void SelfViewModeOff(); - /// - /// Toggles selfview mode - /// - void SelfViewModeToggle(); - } -} \ No newline at end of file + /// + /// Toggles selfview mode + /// + void SelfViewModeToggle(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingInfo.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingInfo.cs index 6ec1f8da..ef0c72ed 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingInfo.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingInfo.cs @@ -2,21 +2,21 @@ using System; -namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + + +/// +/// Describes a device that provides meeting information (like a ZoomRoom) +/// +public interface IHasMeetingInfo { /// - /// Describes a device that provides meeting information (like a ZoomRoom) + /// Raised when meeting info changes /// - public interface IHasMeetingInfo - { - /// - /// Raised when meeting info changes - /// - event EventHandler MeetingInfoChanged; + event EventHandler MeetingInfoChanged; - /// - /// Gets the current meeting information - /// - MeetingInfo MeetingInfo { get; } - } -} \ No newline at end of file + /// + /// Gets the current meeting information + /// + MeetingInfo MeetingInfo { get; } +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingLock.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingLock.cs index b133eebd..de4dbdd5 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingLock.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingLock.cs @@ -1,30 +1,29 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + +/// +/// Defines the contract for IHasMeetingLock +/// +public interface IHasMeetingLock { /// - /// Defines the contract for IHasMeetingLock + /// Feedback that indicates whether the meeting is locked /// - public interface IHasMeetingLock - { - /// - /// Feedback that indicates whether the meeting is locked - /// - BoolFeedback MeetingIsLockedFeedback { get; } + BoolFeedback MeetingIsLockedFeedback { get; } - /// - /// Locks the meeting - /// - void LockMeeting(); + /// + /// Locks the meeting + /// + void LockMeeting(); - /// - /// Unlocks the meeting - /// - void UnLockMeeting(); + /// + /// Unlocks the meeting + /// + void UnLockMeeting(); - /// - /// Toggles the meeting lock state - /// - void ToggleMeetingLock(); - } -} \ No newline at end of file + /// + /// Toggles the meeting lock state + /// + void ToggleMeetingLock(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingRecording.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingRecording.cs index 96e23daa..6ec4c436 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingRecording.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasMeetingRecording.cs @@ -1,30 +1,30 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + + +/// +/// Defines the contract for IHasMeetingRecording +/// +public interface IHasMeetingRecording { /// - /// Defines the contract for IHasMeetingRecording + /// Feedback that indicates whether the meeting is being recorded /// - public interface IHasMeetingRecording - { - /// - /// Feedback that indicates whether the meeting is being recorded - /// - BoolFeedback MeetingIsRecordingFeedback { get; } + BoolFeedback MeetingIsRecordingFeedback { get; } - /// - /// Starts recording the meeting - /// - void StartRecording(); + /// + /// Starts recording the meeting + /// + void StartRecording(); - /// - /// Stops recording the meeting - /// - void StopRecording(); + /// + /// Stops recording the meeting + /// + void StopRecording(); - /// - /// Toggles recording the meeting - /// - void ToggleRecording(); - } -} \ No newline at end of file + /// + /// Toggles recording the meeting + /// + void ToggleRecording(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasParticipants.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasParticipants.cs index 6ff549af..42869ee9 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasParticipants.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasParticipants.cs @@ -28,4 +28,4 @@ /// void AdmitParticipantFromWaitingRoom(int userId); } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasPresentationOnlyMeeting.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasPresentationOnlyMeeting.cs index b2180c39..c81152be 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasPresentationOnlyMeeting.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasPresentationOnlyMeeting.cs @@ -1,58 +1,57 @@ -namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + +/// +/// Defines the contract for IHasPresentationOnlyMeeting +/// +public interface IHasPresentationOnlyMeeting { /// - /// Defines the contract for IHasPresentationOnlyMeeting + /// Starts a presentation only meeting /// - public interface IHasPresentationOnlyMeeting - { - /// - /// Starts a presentation only meeting - /// - void StartSharingOnlyMeeting(); - - /// - /// Starts a presentation only meeting with specified display mode - /// - /// The display mode for the meeting - void StartSharingOnlyMeeting(eSharingMeetingMode displayMode); - - /// - /// Starts a presentation only meeting with specified display mode and duration - /// - /// The display mode for the meeting - /// The duration for the meeting - void StartSharingOnlyMeeting(eSharingMeetingMode displayMode, uint duration); - - /// - /// Starts a presentation only meeting with specified display mode, duration, and password - /// - /// The display mode for the meeting - /// The duration for the meeting - /// The password for the meeting - void StartSharingOnlyMeeting(eSharingMeetingMode displayMode, uint duration, string password); - - /// - /// Starts a normal meeting from a sharing only meeting - /// - void StartNormalMeetingFromSharingOnlyMeeting(); - } + void StartSharingOnlyMeeting(); /// - /// Enumeration of eSharingMeetingMode values + /// Starts a presentation only meeting with specified display mode /// - public enum eSharingMeetingMode - { - /// - /// No specific sharing mode - /// - None, - /// - /// Laptop sharing mode - /// - Laptop, - /// - /// iOS sharing mode - /// - Ios, - } -} \ No newline at end of file + /// The display mode for the meeting + void StartSharingOnlyMeeting(eSharingMeetingMode displayMode); + + /// + /// Starts a presentation only meeting with specified display mode and duration + /// + /// The display mode for the meeting + /// The duration for the meeting + void StartSharingOnlyMeeting(eSharingMeetingMode displayMode, uint duration); + + /// + /// Starts a presentation only meeting with specified display mode, duration, and password + /// + /// The display mode for the meeting + /// The duration for the meeting + /// The password for the meeting + void StartSharingOnlyMeeting(eSharingMeetingMode displayMode, uint duration, string password); + + /// + /// Starts a normal meeting from a sharing only meeting + /// + void StartNormalMeetingFromSharingOnlyMeeting(); +} + +/// +/// Enumeration of eSharingMeetingMode values +/// +public enum eSharingMeetingMode +{ + /// + /// No specific sharing mode + /// + None, + /// + /// Laptop sharing mode + /// + Laptop, + /// + /// iOS sharing mode + /// + Ios, +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasSelfviewPosition.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasSelfviewPosition.cs index 46c3d8f7..3e9b861c 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasSelfviewPosition.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasSelfviewPosition.cs @@ -1,25 +1,25 @@ using PepperDash.Essentials.Devices.Common.VideoCodec; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +/// +/// Defines the contract for IHasSelfviewPosition +/// +public interface IHasSelfviewPosition { /// - /// Defines the contract for IHasSelfviewPosition + /// Gets the SelfviewPipPositionFeedback /// - public interface IHasSelfviewPosition - { - /// - /// Gets the SelfviewPipPositionFeedback - /// - StringFeedback SelfviewPipPositionFeedback { get; } + StringFeedback SelfviewPipPositionFeedback { get; } - /// - /// Sets the selfview position - /// - void SelfviewPipPositionSet(CodecCommandWithLabel position); + /// + /// Sets the selfview position + /// + void SelfviewPipPositionSet(CodecCommandWithLabel position); - /// - /// Toggles the selfview position - /// - void SelfviewPipPositionToggle(); - } -} \ No newline at end of file + /// + /// Toggles the selfview position + /// + void SelfviewPipPositionToggle(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasSelfviewSize.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasSelfviewSize.cs index ab08a414..505dca64 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasSelfviewSize.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasSelfviewSize.cs @@ -1,26 +1,25 @@ using PepperDash.Essentials.Devices.Common.VideoCodec; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Defines the contract for IHasSelfviewSize +/// +public interface IHasSelfviewSize { /// - /// Defines the contract for IHasSelfviewSize + /// Gets the SelfviewPipSizeFeedback /// - public interface IHasSelfviewSize - { - /// - /// Gets the SelfviewPipSizeFeedback - /// - StringFeedback SelfviewPipSizeFeedback { get; } + StringFeedback SelfviewPipSizeFeedback { get; } - /// - /// Sets the selfview size - /// - /// The new selfview size - void SelfviewPipSizeSet(CodecCommandWithLabel size); + /// + /// Sets the selfview size + /// + /// The new selfview size + void SelfviewPipSizeSet(CodecCommandWithLabel size); - /// - /// Toggles the selfview size - /// - void SelfviewPipSizeToggle(); - } + /// + /// Toggles the selfview size + /// + void SelfviewPipSizeToggle(); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasStandbyMode.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasStandbyMode.cs index d0f1333b..58247ae7 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasStandbyMode.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasStandbyMode.cs @@ -1,46 +1,46 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + + +/// +/// Describes a device that has Standby Mode capability +/// +public interface IHasStandbyMode { /// - /// Describes a device that has Standby Mode capability + /// Feedback that indicates whether Standby Mode is on /// - public interface IHasStandbyMode - { - /// - /// Feedback that indicates whether Standby Mode is on - /// - BoolFeedback StandbyIsOnFeedback { get; } - - /// - /// Activates Standby Mode - /// - void StandbyActivate(); - - /// - /// Deactivates Standby Mode - /// - void StandbyDeactivate(); - } + BoolFeedback StandbyIsOnFeedback { get; } /// - /// Defines the contract for IHasHalfWakeMode + /// Activates Standby Mode /// - public interface IHasHalfWakeMode : IHasStandbyMode - { - /// - /// Feedback that indicates whether Half Wake Mode is on - /// - BoolFeedback HalfWakeModeIsOnFeedback { get; } + void StandbyActivate(); - /// - /// Feedback that indicates whether the device is entering Standby Mode - /// - BoolFeedback EnteringStandbyModeFeedback { get; } + /// + /// Deactivates Standby Mode + /// + void StandbyDeactivate(); +} - /// - /// Activates Half Wake Mode - /// - void HalfwakeActivate(); - } -} \ No newline at end of file +/// +/// Defines the contract for IHasHalfWakeMode +/// +public interface IHasHalfWakeMode : IHasStandbyMode +{ + /// + /// Feedback that indicates whether Half Wake Mode is on + /// + BoolFeedback HalfWakeModeIsOnFeedback { get; } + + /// + /// Feedback that indicates whether the device is entering Standby Mode + /// + BoolFeedback EnteringStandbyModeFeedback { get; } + + /// + /// Activates Half Wake Mode + /// + void HalfwakeActivate(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasStartMeeting.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasStartMeeting.cs index 357416e2..2e2f0266 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasStartMeeting.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasStartMeeting.cs @@ -1,24 +1,23 @@ -namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces +namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + +/// +/// Describes the ability to start an ad-hoc meeting +/// +public interface IHasStartMeeting { /// - /// Describes the ability to start an ad-hoc meeting + /// The default meeting duration in minutes /// - public interface IHasStartMeeting - { - /// - /// The default meeting duration in minutes - /// - uint DefaultMeetingDurationMin { get; } + uint DefaultMeetingDurationMin { get; } - /// - /// Start an ad-hoc meeting for the specified duration - /// - /// - void StartMeeting(uint duration); + /// + /// Start an ad-hoc meeting for the specified duration + /// + /// + void StartMeeting(uint duration); - /// - /// Leaves a meeting without ending it - /// - void LeaveMeeting(); - } + /// + /// Leaves a meeting without ending it + /// + void LeaveMeeting(); } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasVideoCodec.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasVideoCodec.cs index cdeb9190..91d2fba9 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasVideoCodec.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IHasVideoCodec.cs @@ -1,26 +1,25 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + +/// +/// For rooms that have video codec +/// +public interface IHasVideoCodec : IHasInCallFeedback, IPrivacy { /// - /// For rooms that have video codec + /// Gets the VideoCodecBase instance /// - public interface IHasVideoCodec : IHasInCallFeedback, IPrivacy - { - /// - /// Gets the VideoCodecBase instance - /// - VideoCodecBase VideoCodec { get; } + VideoCodecBase VideoCodec { get; } - /// - /// States: 0 for on hook, 1 for video, 2 for audio, 3 for telekenesis - /// - IntFeedback CallTypeFeedback { get; } + /// + /// States: 0 for on hook, 1 for video, 2 for audio, 3 for telekenesis + /// + IntFeedback CallTypeFeedback { get; } - /// - /// When something in the room is sharing with the far end or through other means - /// - BoolFeedback IsSharingFeedback { get; } + /// + /// When something in the room is sharing with the far end or through other means + /// + BoolFeedback IsSharingFeedback { get; } - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IJoinCalls.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IJoinCalls.cs index b2583a5d..bf0fe51c 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IJoinCalls.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/IJoinCalls.cs @@ -1,21 +1,21 @@ using PepperDash.Essentials.Devices.Common.Codec; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + + +/// +/// Defines the contract for IJoinCalls +/// +public interface IJoinCalls { /// - /// Defines the contract for IJoinCalls + /// Joins a call /// - public interface IJoinCalls - { - /// - /// Joins a call - /// - /// The active call to join - void JoinCall(CodecActiveCallItem activeCall); + /// The active call to join + void JoinCall(CodecActiveCallItem activeCall); - /// - /// Joins all calls - /// - void JoinAllCalls(); - } -} \ No newline at end of file + /// + /// Joins all calls + /// + void JoinAllCalls(); +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVCCamera.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVCCamera.cs index 5c7e89d8..d2fba918 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVCCamera.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVCCamera.cs @@ -4,299 +4,198 @@ using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Devices.Common.VideoCodec; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common.Cameras +namespace PepperDash.Essentials.Devices.Common.Cameras; + +public class MockVCCamera : CameraBase, IHasCameraPtzControl, IHasCameraFocusControl, IBridgeAdvanced { - /// - /// Represents a MockVCCamera - /// - public class MockVCCamera : CameraBase, IHasCameraPtzControl, IHasCameraFocusControl, IBridgeAdvanced + protected VideoCodecBase ParentCodec { get; private set; } + + + public MockVCCamera(string key, string name, VideoCodecBase codec) + : base(key, name) { - /// - /// Gets the parent video codec - /// - protected VideoCodecBase ParentCodec { get; private set; } + Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus; - /// - /// Initializes a new instance of the MockVCCamera class - /// - /// The device key - /// The device name - /// The parent video codec - public MockVCCamera(string key, string name, VideoCodecBase codec) - : base(key, name) - { - Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus; - - ParentCodec = codec; - } - - #region IHasCameraPtzControl Members - - /// - /// PositionHome method - /// - public void PositionHome() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Resetting to home position"); - } - - #endregion - - #region IHasCameraPanControl Members - - /// - /// PanLeft method - /// - public void PanLeft() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Panning Left"); - } - - /// - /// PanRight method - /// - public void PanRight() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Panning Right"); - } - - /// - /// PanStop method - /// - public void PanStop() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Pan"); - } - - #endregion - - #region IHasCameraTiltControl Members - - /// - /// TiltDown method - /// - public void TiltDown() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Tilting Down"); - } - - /// - /// TiltUp method - /// - public void TiltUp() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Tilting Up"); - } - - /// - /// TiltStop method - /// - public void TiltStop() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Tilt"); - } - - #endregion - - #region IHasCameraZoomControl Members - - /// - /// ZoomIn method - /// - public void ZoomIn() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Zooming In"); - } - - /// - /// ZoomOut method - /// - public void ZoomOut() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Zooming Out"); - } - - /// - /// ZoomStop method - /// - public void ZoomStop() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Zoom"); - } - - #endregion - - #region IHasCameraFocusControl Members - - /// - /// FocusNear method - /// - public void FocusNear() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Focusing Near"); - } - - /// - /// FocusFar method - /// - public void FocusFar() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Focusing Far"); - } - - /// - /// FocusStop method - /// - public void FocusStop() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Focus"); - } - - /// - /// TriggerAutoFocus method - /// - public void TriggerAutoFocus() - { - Debug.LogMessage(LogEventLevel.Debug, this, "AutoFocus Triggered"); - } - - #endregion - - /// - /// LinkToApi method - /// - public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge); - } + ParentCodec = codec; } - /// - /// Represents a MockFarEndVCCamera - /// - public class MockFarEndVCCamera : CameraBase, IHasCameraPtzControl, IAmFarEndCamera, IBridgeAdvanced + #region IHasCameraPtzControl Members + + public void PositionHome() { - /// - /// Gets the parent video codec - /// - protected VideoCodecBase ParentCodec { get; private set; } + Debug.LogMessage(LogEventLevel.Debug, this, "Resetting to home position"); + } - /// - /// Initializes a new instance of the MockFarEndVCCamera class - /// - /// The device key - /// The device name - /// The parent video codec - public MockFarEndVCCamera(string key, string name, VideoCodecBase codec) - : base(key, name) - { - Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom; + #endregion - ParentCodec = codec; - } + #region IHasCameraPanControl Members - #region IHasCameraPtzControl Members + public void PanLeft() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Panning Left"); + } - /// - /// PositionHome method - /// - public void PositionHome() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Resetting to home position"); - } + public void PanRight() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Panning Right"); + } - #endregion + public void PanStop() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Pan"); + } - #region IHasCameraPanControl Members + #endregion - /// - /// PanLeft method - /// - public void PanLeft() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Panning Left"); - } + #region IHasCameraTiltControl Members - /// - /// PanRight method - /// - public void PanRight() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Panning Right"); - } + public void TiltDown() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Tilting Down"); + } - /// - /// PanStop method - /// - public void PanStop() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Pan"); - } + public void TiltUp() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Tilting Up"); + } - #endregion + public void TiltStop() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Tilt"); + } - #region IHasCameraTiltControl Members + #endregion - /// - /// TiltDown method - /// - public void TiltDown() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Tilting Down"); - } + #region IHasCameraZoomControl Members - /// - /// TiltUp method - /// - public void TiltUp() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Tilting Up"); - } + public void ZoomIn() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Zooming In"); + } - /// - /// TiltStop method - /// - public void TiltStop() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Tilt"); - } + public void ZoomOut() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Zooming Out"); + } - #endregion + public void ZoomStop() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Zoom"); + } - #region IHasCameraZoomControl Members + #endregion - /// - /// ZoomIn method - /// - public void ZoomIn() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Zooming In"); - } + #region IHasCameraFocusControl Members - /// - /// ZoomOut method - /// - public void ZoomOut() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Zooming Out"); - } + public void FocusNear() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Focusing Near"); + } - /// - /// ZoomStop method - /// - public void ZoomStop() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Zoom"); - } + public void FocusFar() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Focusing Far"); + } - #endregion + public void FocusStop() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Focus"); + } - /// - /// LinkToApi method - /// - public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge); - } + public void TriggerAutoFocus() + { + Debug.LogMessage(LogEventLevel.Debug, this, "AutoFocus Triggered"); + } + + #endregion + + public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge); + } +} + +public class MockFarEndVCCamera : CameraBase, IHasCameraPtzControl, IAmFarEndCamera, IBridgeAdvanced +{ + protected VideoCodecBase ParentCodec { get; private set; } + + + public MockFarEndVCCamera(string key, string name, VideoCodecBase codec) + : base(key, name) + { + Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom; + + ParentCodec = codec; + } + + #region IHasCameraPtzControl Members + + public void PositionHome() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Resetting to home position"); + } + + #endregion + + #region IHasCameraPanControl Members + + public void PanLeft() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Panning Left"); + } + + public void PanRight() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Panning Right"); + } + + public void PanStop() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Pan"); + } + + #endregion + + #region IHasCameraTiltControl Members + + public void TiltDown() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Tilting Down"); + } + + public void TiltUp() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Tilting Up"); + } + + public void TiltStop() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Tilt"); + } + + #endregion + + #region IHasCameraZoomControl Members + + public void ZoomIn() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Zooming In"); + } + + public void ZoomOut() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Zooming Out"); + } + + public void ZoomStop() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Stopping Zoom"); + } + + #endregion + + public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) + { + LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs index 0c5654d6..aa0bcadd 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVcPropertiesConfig.cs @@ -2,32 +2,32 @@ using Newtonsoft.Json; using PepperDash.Essentials.Devices.Common.Codec; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + + +/// +/// Represents a MockVcPropertiesConfig +/// +public class MockVcPropertiesConfig { /// - /// Represents a MockVcPropertiesConfig + /// Gets or sets the Favorites /// - public class MockVcPropertiesConfig + [JsonProperty("favorites")] + public List Favorites { get; set; } + + /// + /// Gets or sets the Presets + /// + [JsonProperty("presets")] + public List Presets { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public MockVcPropertiesConfig() { - /// - /// Gets or sets the Favorites - /// - [JsonProperty("favorites")] - public List Favorites { get; set; } - - /// - /// Gets or sets the Presets - /// - [JsonProperty("presets")] - public List Presets { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public MockVcPropertiesConfig() - { - Favorites = new List(); - Presets = new List(); - } + Favorites = new List(); + Presets = new List(); } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index fbc73852..65d89d85 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -21,674 +21,641 @@ using Serilog.Events; using Feedback = PepperDash.Essentials.Core.Feedback; using System.Text; -namespace PepperDash.Essentials.Devices.Common.VideoCodec +namespace PepperDash.Essentials.Devices.Common.VideoCodec; + +public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs, + IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode { + private const int XSigEncoding = 28591; + protected const int MaxParticipants = 50; + private readonly byte[] _clearBytes = XSigHelpers.ClearOutputs(); + + private readonly IHasDirectory _directoryCodec; + private readonly BasicTriList _directoryTrilist; + private readonly VideoCodecControllerJoinMap _directoryJoinmap; + + protected string _timeFormatSpecifier; + /// - /// Base class for video codec devices + /// Date format specifier /// - public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs, - IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode + protected string _dateFormatSpecifier; + + /// + /// Initializes a new instance of the class. + /// + protected VideoCodecBase(DeviceConfig config) + : base(config) { - private const int XSigEncoding = 28591; - /// - /// Maximum number of participants - /// - protected const int MaxParticipants = 50; - private readonly byte[] _clearBytes = XSigHelpers.ClearOutputs(); + StandbyIsOnFeedback = new BoolFeedback("standbyIsOn", StandbyIsOnFeedbackFunc); + PrivacyModeIsOnFeedback = new BoolFeedback("privacyModeIsOn", PrivacyModeIsOnFeedbackFunc); + VolumeLevelFeedback = new IntFeedback("volumeLevel", VolumeLevelFeedbackFunc); + MuteFeedback = new BoolFeedback("mute", MuteFeedbackFunc); + SharingSourceFeedback = new StringFeedback("sharingSource", SharingSourceFeedbackFunc); + SharingContentIsOnFeedback = new BoolFeedback("sharingContentIsOn", SharingContentIsOnFeedbackFunc); - /// - /// Time format specifier - /// - protected string _timeFormatSpecifier; + // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + MeetingsToDisplayFeedback = new IntFeedback(() => MeetingsToDisplay); - /// - /// Date format specifier - /// - protected string _dateFormatSpecifier; + InputPorts = new RoutingPortCollection(); + OutputPorts = new RoutingPortCollection(); - /// - /// Initializes a new instance of the class. - /// - protected VideoCodecBase(DeviceConfig config) - : base(config) + ActiveCalls = new List(); + } + + /// + /// Gets or sets the Communication + /// + public IBasicCommunication Communication { get; protected set; } + + /// + /// Gets or sets the OsdSource + /// + public DummyRoutingInputsDevice OsdSource { get; protected set; } + + /// + /// Gets or sets the StandbyIsOnFeedback + /// + public BoolFeedback StandbyIsOnFeedback { get; private set; } + + /// + /// Gets or sets the PrivacyModeIsOnFeedbackFunc + /// + protected abstract Func PrivacyModeIsOnFeedbackFunc { get; } + + /// + /// Gets or sets the VolumeLevelFeedbackFunc + /// + protected abstract Func VolumeLevelFeedbackFunc { get; } + + /// + /// Gets or sets the MuteFeedbackFunc + /// + protected abstract Func MuteFeedbackFunc { get; } + + /// + /// Gets or sets the StandbyIsOnFeedbackFunc + /// + protected abstract Func StandbyIsOnFeedbackFunc { get; } + + /// + /// Gets or sets the ActiveCalls + /// + public List ActiveCalls { get; set; } + + /// + /// Gets or sets the ShowSelfViewByDefault + /// + public bool ShowSelfViewByDefault { get; protected set; } + + public bool SupportsCameraOff { get; protected set; } + public bool SupportsCameraAutoMode { get; protected set; } + + /// + /// Gets or sets the IsReady + /// + public bool IsReady { get; protected set; } + + /// + public virtual List Feedbacks + { + get { - - StandbyIsOnFeedback = new BoolFeedback("standbyIsOn", StandbyIsOnFeedbackFunc); - PrivacyModeIsOnFeedback = new BoolFeedback("privacyModeIsOn", PrivacyModeIsOnFeedbackFunc); - VolumeLevelFeedback = new IntFeedback("volumeLevel", VolumeLevelFeedbackFunc); - MuteFeedback = new BoolFeedback("mute", MuteFeedbackFunc); - SharingSourceFeedback = new StringFeedback("sharingSource", SharingSourceFeedbackFunc); - SharingContentIsOnFeedback = new BoolFeedback("sharingContentIsOn", SharingContentIsOnFeedbackFunc); - - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set - MeetingsToDisplayFeedback = new IntFeedback("meetingsToDisplay", () => MeetingsToDisplay); - - InputPorts = new RoutingPortCollection(); - OutputPorts = new RoutingPortCollection(); - - ActiveCalls = new List(); - } - - /// - /// Gets or sets the Communication - /// - public IBasicCommunication Communication { get; protected set; } - - /// - /// Gets or sets the OsdSource - /// - public DummyRoutingInputsDevice OsdSource { get; protected set; } - - /// - /// Gets or sets the StandbyIsOnFeedback - /// - public BoolFeedback StandbyIsOnFeedback { get; private set; } - - /// - /// Gets or sets the PrivacyModeIsOnFeedbackFunc - /// - protected abstract Func PrivacyModeIsOnFeedbackFunc { get; } - - /// - /// Gets or sets the VolumeLevelFeedbackFunc - /// - protected abstract Func VolumeLevelFeedbackFunc { get; } - - /// - /// Gets or sets the MuteFeedbackFunc - /// - protected abstract Func MuteFeedbackFunc { get; } - - /// - /// Gets or sets the StandbyIsOnFeedbackFunc - /// - protected abstract Func StandbyIsOnFeedbackFunc { get; } - - /// - /// Gets or sets the ActiveCalls - /// - public List ActiveCalls { get; set; } - - /// - /// Gets or sets the ShowSelfViewByDefault - /// - public bool ShowSelfViewByDefault { get; protected set; } - - /// - /// Gets or sets the SupportsCameraOff - /// - public bool SupportsCameraOff { get; protected set; } - /// - /// Gets or sets the SupportsCameraAutoMode - /// - public bool SupportsCameraAutoMode { get; protected set; } - - /// - /// Gets or sets the IsReady - /// - public bool IsReady { get; protected set; } - - /// - public virtual List Feedbacks - { - get + return new List { - return new List - { - PrivacyModeIsOnFeedback, - SharingSourceFeedback - }; - } - } - - /// - /// Gets or sets the MeetingsToDisplay - /// - protected abstract Func SharingSourceFeedbackFunc { get; } - - /// - /// Gets or sets the SharingContentIsOnFeedbackFunc - /// - protected abstract Func SharingContentIsOnFeedbackFunc { get; } - - #region ICodecAudio Members - - /// - /// Set Privacy Mode On - /// - public abstract void PrivacyModeOn(); - - /// - /// Set Privacy Mode Off - /// - public abstract void PrivacyModeOff(); - - /// - /// Toggles the Privacy Mode - /// - public abstract void PrivacyModeToggle(); - - /// - /// Gets or sets the PrivacyModeIsOnFeedback - /// - public BoolFeedback PrivacyModeIsOnFeedback { get; private set; } - - /// - /// Gets or sets the MuteFeedback - /// - public BoolFeedback MuteFeedback { get; private set; } - - /// - /// Sets the Mute state to Off - /// - public abstract void MuteOff(); - - /// - /// Sets the Mute state to On - /// - public abstract void MuteOn(); - - /// - /// Sets the Volume level - /// - public abstract void SetVolume(ushort level); - - /// - /// Gets or sets the VolumeLevelFeedback - /// - public IntFeedback VolumeLevelFeedback { get; private set; } - - /// - /// Toggles the Mute state - /// - public abstract void MuteToggle(); - - /// - /// Decreases the Volume level - /// - public abstract void VolumeDown(bool pressRelease); - - /// - /// Increases the Volume level - /// - public abstract void VolumeUp(bool pressRelease); - - #endregion - - #region IHasContentSharing Members - - /// - /// Starts content sharing - /// - public abstract void StartSharing(); - - /// - /// Stops content sharing - /// - public abstract void StopSharing(); - - /// - /// Gets or sets the AutoShareContentWhileInCall - /// - public bool AutoShareContentWhileInCall { get; protected set; } - - /// - /// Gets or sets the SharingSourceFeedback - /// - public StringFeedback SharingSourceFeedback { get; private set; } - - /// - /// Gets or sets the SharingContentIsOnFeedback - /// - public BoolFeedback SharingContentIsOnFeedback { get; private set; } - - #endregion - - #region IHasDialer Members - - /// - /// Fires when the status of any active, dialing, or incoming call changes or is new - /// - public event EventHandler CallStatusChange; - - /// - /// Returns true when any call is not in state Unknown, Disconnecting, Disconnected - /// - public bool IsInCall - { - get - { - var value = ActiveCalls != null && ActiveCalls.Any(c => c.IsActiveCall); - return value; - } - } - - /// - /// Dials the specified number - /// - public abstract void Dial(string number); - - /// - /// Ends the specified call - /// - public abstract void EndCall(CodecActiveCallItem call); - - /// - /// Ends all active calls - /// - public abstract void EndAllCalls(); - - /// - /// Accepts the specified call - /// - public abstract void AcceptCall(CodecActiveCallItem call); - - /// - /// Rejects the specified call - /// - public abstract void RejectCall(CodecActiveCallItem call); - - /// - /// Sends DTMF tones - /// - public abstract void SendDtmf(string s); - - /// - /// SendDtmf method - /// - public virtual void SendDtmf(string s, CodecActiveCallItem call) { } - - #endregion - - #region IRoutingInputsOutputs Members - - /// - /// Gets or sets the InputPorts - /// - public RoutingPortCollection InputPorts { get; private set; } - - /// - /// Gets or sets the OutputPorts - /// - public RoutingPortCollection OutputPorts { get; private set; } - - #endregion - - #region IUsageTracking Members - - /// - /// Gets or sets the UsageTracker - /// - public UsageTracking UsageTracker { get; set; } - - #endregion - - #region iVideoCodecInfo Members - - /// - /// Gets the CodecInfo - /// - public VideoCodecInfo CodecInfo { get; protected set; } - - #endregion - - /// - /// Fired when the Codec is ready to be used - /// - public event EventHandler IsReadyChange; - - /// - /// Dials the specified meeting - /// - /// - public abstract void Dial(Meeting meeting); - - /// - /// Dial a contact - /// - /// The contact to dial - public virtual void Dial(IInvitableContact contact) - { - } - - /// - public abstract void ExecuteSwitch(object selector); - - /// - /// Helper method to fire CallStatusChange event with old and new status - /// - protected void SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus newStatus, CodecActiveCallItem call) - { - call.Status = newStatus; - - OnCallStatusChange(call); - } - - /// - /// Sets the previous and new status for the call - /// - /// - protected virtual void OnCallStatusChange(CodecActiveCallItem item) - { - CallStatusChange?.Invoke(this, new CodecCallStatusItemChangeEventArgs(item)); - - PrivacyModeIsOnFeedback.FireUpdate(); - - if (AutoShareContentWhileInCall) - { - StartSharing(); - } - - if (UsageTracker != null) - { - if (IsInCall && !UsageTracker.UsageTrackingStarted) - { - UsageTracker.StartDeviceUsage(); - } - else if (UsageTracker.UsageTrackingStarted && !IsInCall) - { - UsageTracker.EndDeviceUsage(); - } - } - } - - /// - /// Sets IsReady property and fires the event. Used for dependent classes to sync up their data. - /// - protected void SetIsReady() - { - CrestronInvoke.BeginInvoke((o) => - { - try - { - IsReady = true; - IsReadyChange?.Invoke(this, new EventArgs()); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Error in SetIsReady() : {0}", e); - } - }); - } - - // **** DEBUGGING THINGS **** - /// - /// ListCalls method - /// - public virtual void ListCalls() - { - Debug.LogMessage(LogEventLevel.Debug, this, "Active Calls:"); - - var sb = new StringBuilder(); - foreach (var c in ActiveCalls) - { - sb.AppendFormat("id: {0} number: {1} -- name: {2} status: {3} onHold: {4}\r\n", c.Id, c.Number, c.Name, c.Status, c.IsOnHold); - } - Debug.LogMessage(LogEventLevel.Debug, this, "\n{0}\n", sb.ToString()); - } - - /// - /// Activate standby - /// - public abstract void StandbyActivate(); - - /// - /// Deactivate standby - /// - public abstract void StandbyDeactivate(); - - #region Implementation of IBridgeAdvanced - - /// - public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); - - /// - /// Use this method when using a plain VideoCodecControllerJoinMap - /// - /// codec to link - /// trilist to link - /// join to start with - /// key for custom join maps - /// bridge controller - protected void LinkVideoCodecToApi(VideoCodecBase codec, BasicTriList trilist, uint joinStart, string joinMapKey, - EiscApiAdvanced bridge) - { - var joinMap = new VideoCodecControllerJoinMap(joinStart); - - var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey); - - if (customJoins != null) - { - joinMap.SetCustomJoinData(customJoins); - } - - bridge?.AddJoinMap(Key, joinMap); - - LinkVideoCodecToApi(codec, trilist, joinMap); - - trilist.OnlineStatusChange += (device, args) => - { - if (!args.DeviceOnLine) return; + PrivacyModeIsOnFeedback, + SharingSourceFeedback }; } + } - /// - /// Use this method when you need to pass in a join map that extends VideoCodecControllerJoinMap - /// - /// codec to link - /// trilist to link - /// join map to use - protected void LinkVideoCodecToApi(VideoCodecBase codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + /// + /// Gets or sets the MeetingsToDisplay + /// + protected abstract Func SharingSourceFeedbackFunc { get; } + + /// + /// Gets or sets the SharingContentIsOnFeedbackFunc + /// + protected abstract Func SharingContentIsOnFeedbackFunc { get; } + + #region ICodecAudio Members + + /// + /// Set Privacy Mode On + /// + public abstract void PrivacyModeOn(); + + /// + /// Set Privacy Mode Off + /// + public abstract void PrivacyModeOff(); + + /// + /// Toggles the Privacy Mode + /// + public abstract void PrivacyModeToggle(); + + /// + /// Gets or sets the PrivacyModeIsOnFeedback + /// + public BoolFeedback PrivacyModeIsOnFeedback { get; private set; } + + /// + /// Gets or sets the MuteFeedback + /// + public BoolFeedback MuteFeedback { get; private set; } + + /// + /// Sets the Mute state to Off + /// + public abstract void MuteOff(); + + /// + /// Sets the Mute state to On + /// + public abstract void MuteOn(); + + /// + /// Sets the Volume level + /// + public abstract void SetVolume(ushort level); + + /// + /// Gets or sets the VolumeLevelFeedback + /// + public IntFeedback VolumeLevelFeedback { get; private set; } + + /// + /// Toggles the Mute state + /// + public abstract void MuteToggle(); + + /// + /// Decreases the Volume level + /// + public abstract void VolumeDown(bool pressRelease); + + /// + /// Increases the Volume level + /// + public abstract void VolumeUp(bool pressRelease); + + #endregion + + #region IHasContentSharing Members + + /// + /// Starts content sharing + /// + public abstract void StartSharing(); + + /// + /// Stops content sharing + /// + public abstract void StopSharing(); + + /// + /// Gets or sets the AutoShareContentWhileInCall + /// + public bool AutoShareContentWhileInCall { get; protected set; } + + /// + /// Gets or sets the SharingSourceFeedback + /// + public StringFeedback SharingSourceFeedback { get; private set; } + + /// + /// Gets or sets the SharingContentIsOnFeedback + /// + public BoolFeedback SharingContentIsOnFeedback { get; private set; } + + #endregion + + #region IHasDialer Members + + /// + /// Fires when the status of any active, dialing, or incoming call changes or is new + /// + public event EventHandler CallStatusChange; + + /// + /// Returns true when any call is not in state Unknown, Disconnecting, Disconnected + /// + public bool IsInCall + { + get { - Debug.LogMessage(LogEventLevel.Debug, this, "Linking to Trilist {0}", trilist.ID.ToString("X")); + var value = ActiveCalls != null && ActiveCalls.Any(c => c.IsActiveCall); + return value; + } + } - LinkVideoCodecDtmfToApi(trilist, joinMap); + /// + /// Dials the specified number + /// + public abstract void Dial(string number); - LinkVideoCodecCallControlsToApi(trilist, joinMap); + /// + /// Ends the specified call + /// + public abstract void EndCall(CodecActiveCallItem call); - LinkVideoCodecContentSharingToApi(trilist, joinMap); + /// + /// Ends all active calls + /// + public abstract void EndAllCalls(); - LinkVideoCodecPrivacyToApi(trilist, joinMap); + /// + /// Accepts the specified call + /// + public abstract void AcceptCall(CodecActiveCallItem call); - LinkVideoCodecVolumeToApi(trilist, joinMap); + /// + /// Rejects the specified call + /// + public abstract void RejectCall(CodecActiveCallItem call); - LinkVideoCodecInfoToApi(trilist, joinMap); + /// + /// Sends DTMF tones + /// + public abstract void SendDtmf(string s); + public virtual void SendDtmf(string s, CodecActiveCallItem call) { } - // Register for this event to link any functions that require the codec to be ready first - codec.IsReadyChange += (o, a) => - { - if (codec is IHasCodecCameras) - { - LinkVideoCodecCameraToApi(codec as IHasCodecCameras, trilist, joinMap); - } - }; + #endregion - if (codec is ICommunicationMonitor) + #region IRoutingInputsOutputs Members + + /// + /// Gets or sets the InputPorts + /// + public RoutingPortCollection InputPorts { get; private set; } + + /// + /// Gets or sets the OutputPorts + /// + public RoutingPortCollection OutputPorts { get; private set; } + + #endregion + + #region IUsageTracking Members + + /// + /// Gets or sets the UsageTracker + /// + public UsageTracking UsageTracker { get; set; } + + #endregion + + #region iVideoCodecInfo Members + + /// + /// Gets the CodecInfo + /// + public VideoCodecInfo CodecInfo { get; protected set; } + + #endregion + + /// + /// Fired when the Codec is ready to be used + /// + public event EventHandler IsReadyChange; + + /// + /// Dials the specified meeting + /// + /// + public abstract void Dial(Meeting meeting); + + /// + /// Dial a contact + /// + /// The contact to dial + public virtual void Dial(IInvitableContact contact) + { + } + + /// + public abstract void ExecuteSwitch(object selector); + + /// + /// Helper method to fire CallStatusChange event with old and new status + /// + protected void SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus newStatus, CodecActiveCallItem call) + { + call.Status = newStatus; + + OnCallStatusChange(call); + } + + /// + /// Sets the previous and new status for the call + /// + /// + protected virtual void OnCallStatusChange(CodecActiveCallItem item) + { + CallStatusChange?.Invoke(this, new CodecCallStatusItemChangeEventArgs(item)); + + PrivacyModeIsOnFeedback.FireUpdate(); + + if (AutoShareContentWhileInCall) + { + StartSharing(); + } + + if (UsageTracker != null) + { + if (IsInCall && !UsageTracker.UsageTrackingStarted) { - LinkVideoCodecCommMonitorToApi(codec as ICommunicationMonitor, trilist, joinMap); + UsageTracker.StartDeviceUsage(); + } + else if (UsageTracker.UsageTrackingStarted && !IsInCall) + { + UsageTracker.EndDeviceUsage(); + } + } + } + + /// + /// Sets IsReady property and fires the event. Used for dependent classes to sync up their data. + /// + protected void SetIsReady() + { + CrestronInvoke.BeginInvoke((o) => + { + try + { + IsReady = true; + IsReadyChange?.Invoke(this, new EventArgs()); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Error in SetIsReady() : {0}", e); + } + }); + } + + // **** DEBUGGING THINGS **** + /// + /// ListCalls method + /// + public virtual void ListCalls() + { + Debug.LogMessage(LogEventLevel.Debug, this, "Active Calls:"); + + var sb = new StringBuilder(); + foreach (var c in ActiveCalls) + { + sb.AppendFormat("id: {0} number: {1} -- name: {2} status: {3} onHold: {4}\r\n", c.Id, c.Number, c.Name, c.Status, c.IsOnHold); + } + Debug.LogMessage(LogEventLevel.Debug, this, "\n{0}\n", sb.ToString()); + } + + /// + /// Activate standby + /// + public abstract void StandbyActivate(); + + /// + /// Deactivate standby + /// + public abstract void StandbyDeactivate(); + + #region Implementation of IBridgeAdvanced + + /// + public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); + + /// + /// Use this method when using a plain VideoCodecControllerJoinMap + /// + /// codec to link + /// trilist to link + /// join to start with + /// key for custom join maps + /// bridge controller + protected void LinkVideoCodecToApi(VideoCodecBase codec, BasicTriList trilist, uint joinStart, string joinMapKey, + EiscApiAdvanced bridge) + { + var joinMap = new VideoCodecControllerJoinMap(joinStart); + + var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey); + + if (customJoins != null) + { + joinMap.SetCustomJoinData(customJoins); + } + + bridge?.AddJoinMap(Key, joinMap); + + LinkVideoCodecToApi(codec, trilist, joinMap); + + trilist.OnlineStatusChange += (device, args) => + { + if (!args.DeviceOnLine) return; + }; + } + + /// + /// Use this method when you need to pass in a join map that extends VideoCodecControllerJoinMap + /// + /// codec to link + /// trilist to link + /// join map to use + protected void LinkVideoCodecToApi(VideoCodecBase codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Linking to Trilist {0}", trilist.ID.ToString("X")); + + LinkVideoCodecDtmfToApi(trilist, joinMap); + + LinkVideoCodecCallControlsToApi(trilist, joinMap); + + LinkVideoCodecContentSharingToApi(trilist, joinMap); + + LinkVideoCodecPrivacyToApi(trilist, joinMap); + + LinkVideoCodecVolumeToApi(trilist, joinMap); + + LinkVideoCodecInfoToApi(trilist, joinMap); + + // Register for this event to link any functions that require the codec to be ready first + codec.IsReadyChange += (o, a) => + { + if (codec is IHasCodecCameras) + { + LinkVideoCodecCameraToApi(codec as IHasCodecCameras, trilist, joinMap); + } + }; + + if (codec is ICommunicationMonitor) + { + LinkVideoCodecCommMonitorToApi(codec as ICommunicationMonitor, trilist, joinMap); + } + + + if (codec is IHasCodecSelfView) + { + LinkVideoCodecSelfviewToApi(codec as IHasCodecSelfView, trilist, joinMap); + } + + if (codec is IHasCameraAutoMode) + { + trilist.SetBool(joinMap.CameraSupportsAutoMode.JoinNumber, SupportsCameraAutoMode); + LinkVideoCodecCameraModeToApi(codec as IHasCameraAutoMode, trilist, joinMap); + } + + if (codec is IHasCameraOff) + { + trilist.SetBool(joinMap.CameraSupportsOffMode.JoinNumber, SupportsCameraOff); + LinkVideoCodecCameraOffToApi(codec as IHasCameraOff, trilist, joinMap); + } + + if (codec is IHasCodecLayouts) + { + LinkVideoCodecCameraLayoutsToApi(codec as IHasCodecLayouts, trilist, joinMap); + } + + + if (codec is IHasSelfviewPosition) + { + LinkVideoCodecSelfviewPositionToApi(codec as IHasSelfviewPosition, trilist, joinMap); + } + + if (codec is IHasDirectory) + { + LinkVideoCodecDirectoryToApi(codec as IHasDirectory, trilist, joinMap); + } + + if (codec is IHasScheduleAwareness) + { + LinkVideoCodecScheduleToApi(codec as IHasScheduleAwareness, trilist, joinMap); + } + + if (codec is IHasParticipants) + { + LinkVideoCodecParticipantsToApi(codec as IHasParticipants, trilist, joinMap); + } + + if (codec is IHasFarEndContentStatus) + { + (codec as IHasFarEndContentStatus).ReceivingContent.LinkInputSig(trilist.BooleanInput[joinMap.RecievingContent.JoinNumber]); + } + + if (codec is IHasPhoneDialing) + { + LinkVideoCodecPhoneToApi(codec as IHasPhoneDialing, trilist, joinMap); + } + + if (codec is IHasCallHistory) + { + LinkVideoCodecCallHistoryToApi(codec as IHasCallHistory, trilist, joinMap); + } + + trilist.OnlineStatusChange += (device, args) => + { + if (!args.DeviceOnLine) return; + + if (codec is IHasDirectory) + { + (codec as IHasDirectory).SetCurrentDirectoryToRoot(); } - - if (codec is IHasCodecSelfView) + if (codec is IHasScheduleAwareness) { - LinkVideoCodecSelfviewToApi(codec as IHasCodecSelfView, trilist, joinMap); + (codec as IHasScheduleAwareness).GetSchedule(); + } + + if (codec is IHasParticipants) + { + UpdateParticipantsXSig((codec as IHasParticipants).Participants.CurrentParticipants); } if (codec is IHasCameraAutoMode) { trilist.SetBool(joinMap.CameraSupportsAutoMode.JoinNumber, SupportsCameraAutoMode); - LinkVideoCodecCameraModeToApi(codec as IHasCameraAutoMode, trilist, joinMap); + + (codec as IHasCameraAutoMode).CameraAutoModeIsOnFeedback.FireUpdate(); + } + + if (codec is IHasCodecSelfView) + { + (codec as IHasCodecSelfView).SelfviewIsOnFeedback.FireUpdate(); + } + + if (codec is IHasCameraAutoMode) + { + (codec as IHasCameraAutoMode).CameraAutoModeIsOnFeedback.FireUpdate(); } if (codec is IHasCameraOff) { - trilist.SetBool(joinMap.CameraSupportsOffMode.JoinNumber, SupportsCameraOff); - LinkVideoCodecCameraOffToApi(codec as IHasCameraOff, trilist, joinMap); - } - - if (codec is IHasCodecLayouts) - { - LinkVideoCodecCameraLayoutsToApi(codec as IHasCodecLayouts, trilist, joinMap); - } - - - if (codec is IHasSelfviewPosition) - { - LinkVideoCodecSelfviewPositionToApi(codec as IHasSelfviewPosition, trilist, joinMap); - } - - if (codec is IHasDirectory) - { - LinkVideoCodecDirectoryToApi(codec as IHasDirectory, trilist, joinMap); - } - - if (codec is IHasScheduleAwareness) - { - LinkVideoCodecScheduleToApi(codec as IHasScheduleAwareness, trilist, joinMap); - } - - if (codec is IHasParticipants) - { - LinkVideoCodecParticipantsToApi(codec as IHasParticipants, trilist, joinMap); - } - - if (codec is IHasFarEndContentStatus) - { - (codec as IHasFarEndContentStatus).ReceivingContent.LinkInputSig(trilist.BooleanInput[joinMap.RecievingContent.JoinNumber]); + (codec as IHasCameraOff).CameraIsOffFeedback.FireUpdate(); } if (codec is IHasPhoneDialing) { - LinkVideoCodecPhoneToApi(codec as IHasPhoneDialing, trilist, joinMap); + (codec as IHasPhoneDialing).PhoneOffHookFeedback.FireUpdate(); } if (codec is IHasCallHistory) { - LinkVideoCodecCallHistoryToApi(codec as IHasCallHistory, trilist, joinMap); + UpdateCallHistory((codec as IHasCallHistory), trilist, joinMap); } - trilist.OnlineStatusChange += (device, args) => + SharingContentIsOnFeedback.FireUpdate(); + }; + } + + private void LinkVideoCodecInfoToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + trilist.SetBool(joinMap.MultiSiteOptionIsEnabled.JoinNumber, this.CodecInfo.MultiSiteOptionIsEnabled); + trilist.SetBool(joinMap.AutoAnswerEnabled.JoinNumber, this.CodecInfo.AutoAnswerEnabled); + trilist.SetString(joinMap.DeviceIpAddresss.JoinNumber, this.CodecInfo.IpAddress); + trilist.SetString(joinMap.SipPhoneNumber.JoinNumber, this.CodecInfo.SipPhoneNumber); + trilist.SetString(joinMap.E164Alias.JoinNumber, this.CodecInfo.E164Alias); + trilist.SetString(joinMap.H323Id.JoinNumber, this.CodecInfo.H323Id); + trilist.SetString(joinMap.SipUri.JoinNumber, this.CodecInfo.SipUri); + + trilist.OnlineStatusChange += (o, a) => + { + if (a.DeviceOnLine) { - if (!args.DeviceOnLine) return; + trilist.SetBool(joinMap.MultiSiteOptionIsEnabled.JoinNumber, this.CodecInfo.MultiSiteOptionIsEnabled); + trilist.SetBool(joinMap.AutoAnswerEnabled.JoinNumber, this.CodecInfo.AutoAnswerEnabled); + trilist.SetString(joinMap.DeviceIpAddresss.JoinNumber, this.CodecInfo.IpAddress); + trilist.SetString(joinMap.SipPhoneNumber.JoinNumber, this.CodecInfo.SipPhoneNumber); + trilist.SetString(joinMap.E164Alias.JoinNumber, this.CodecInfo.E164Alias); + trilist.SetString(joinMap.H323Id.JoinNumber, this.CodecInfo.H323Id); + trilist.SetString(joinMap.SipUri.JoinNumber, this.CodecInfo.SipUri); + } + }; + } - if (codec is IHasDirectory) - { - (codec as IHasDirectory).SetCurrentDirectoryToRoot(); - } + private void LinkVideoCodecPhoneToApi(IHasPhoneDialing codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + codec.PhoneOffHookFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PhoneHookState.JoinNumber]); - if (codec is IHasScheduleAwareness) - { - (codec as IHasScheduleAwareness).GetSchedule(); - } + trilist.SetSigFalseAction(joinMap.DialPhone.JoinNumber, + () => codec.DialPhoneCall(trilist.StringOutput[joinMap.PhoneDialString.JoinNumber].StringValue)); - if (codec is IHasParticipants) - { - UpdateParticipantsXSig((codec as IHasParticipants).Participants.CurrentParticipants); - } + trilist.SetSigFalseAction(joinMap.HangUpPhone.JoinNumber, codec.EndPhoneCall); + } - if (codec is IHasCameraAutoMode) - { - trilist.SetBool(joinMap.CameraSupportsAutoMode.JoinNumber, SupportsCameraAutoMode); + private void LinkVideoCodecSelfviewPositionToApi(IHasSelfviewPosition codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + trilist.SetSigFalseAction(joinMap.SelfviewPosition.JoinNumber, codec.SelfviewPipPositionToggle); - (codec as IHasCameraAutoMode).CameraAutoModeIsOnFeedback.FireUpdate(); - } + codec.SelfviewPipPositionFeedback.LinkInputSig(trilist.StringInput[joinMap.SelfviewPositionFb.JoinNumber]); + } - if (codec is IHasCodecSelfView) - { - (codec as IHasCodecSelfView).SelfviewIsOnFeedback.FireUpdate(); - } + private void LinkVideoCodecCameraOffToApi(IHasCameraOff codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + trilist.SetSigFalseAction(joinMap.CameraModeOff.JoinNumber, codec.CameraOff); - if (codec is IHasCameraAutoMode) - { - (codec as IHasCameraAutoMode).CameraAutoModeIsOnFeedback.FireUpdate(); - } - - if (codec is IHasCameraOff) - { - (codec as IHasCameraOff).CameraIsOffFeedback.FireUpdate(); - } - - if (codec is IHasPhoneDialing) - { - (codec as IHasPhoneDialing).PhoneOffHookFeedback.FireUpdate(); - } - - if (codec is IHasCallHistory) - { - UpdateCallHistory((codec as IHasCallHistory), trilist, joinMap); - } - - SharingContentIsOnFeedback.FireUpdate(); - }; - } - - private void LinkVideoCodecInfoToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + codec.CameraIsOffFeedback.OutputChange += (o, a) => { - trilist.SetBool(joinMap.MultiSiteOptionIsEnabled.JoinNumber, this.CodecInfo.MultiSiteOptionIsEnabled); - trilist.SetBool(joinMap.AutoAnswerEnabled.JoinNumber, this.CodecInfo.AutoAnswerEnabled); - trilist.SetString(joinMap.DeviceIpAddresss.JoinNumber, this.CodecInfo.IpAddress); - trilist.SetString(joinMap.SipPhoneNumber.JoinNumber, this.CodecInfo.SipPhoneNumber); - trilist.SetString(joinMap.E164Alias.JoinNumber, this.CodecInfo.E164Alias); - trilist.SetString(joinMap.H323Id.JoinNumber, this.CodecInfo.H323Id); - trilist.SetString(joinMap.SipUri.JoinNumber, this.CodecInfo.SipUri); - - trilist.OnlineStatusChange += (o, a) => - { - if (a.DeviceOnLine) - { - trilist.SetBool(joinMap.MultiSiteOptionIsEnabled.JoinNumber, this.CodecInfo.MultiSiteOptionIsEnabled); - trilist.SetBool(joinMap.AutoAnswerEnabled.JoinNumber, this.CodecInfo.AutoAnswerEnabled); - trilist.SetString(joinMap.DeviceIpAddresss.JoinNumber, this.CodecInfo.IpAddress); - trilist.SetString(joinMap.SipPhoneNumber.JoinNumber, this.CodecInfo.SipPhoneNumber); - trilist.SetString(joinMap.E164Alias.JoinNumber, this.CodecInfo.E164Alias); - trilist.SetString(joinMap.H323Id.JoinNumber, this.CodecInfo.H323Id); - trilist.SetString(joinMap.SipUri.JoinNumber, this.CodecInfo.SipUri); - } - }; - } - - private void LinkVideoCodecPhoneToApi(IHasPhoneDialing codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - codec.PhoneOffHookFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PhoneHookState.JoinNumber]); - - trilist.SetSigFalseAction(joinMap.DialPhone.JoinNumber, - () => codec.DialPhoneCall(trilist.StringOutput[joinMap.PhoneDialString.JoinNumber].StringValue)); - - trilist.SetSigFalseAction(joinMap.HangUpPhone.JoinNumber, codec.EndPhoneCall); - } - - private void LinkVideoCodecSelfviewPositionToApi(IHasSelfviewPosition codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - trilist.SetSigFalseAction(joinMap.SelfviewPosition.JoinNumber, codec.SelfviewPipPositionToggle); - - codec.SelfviewPipPositionFeedback.LinkInputSig(trilist.StringInput[joinMap.SelfviewPositionFb.JoinNumber]); - } - - private void LinkVideoCodecCameraOffToApi(IHasCameraOff codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - trilist.SetSigFalseAction(joinMap.CameraModeOff.JoinNumber, codec.CameraOff); - - codec.CameraIsOffFeedback.OutputChange += (o, a) => - { - if (a.BoolValue) - { - trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true); - trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false); - trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false); - return; - } - - trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); - - - if (!(codec is IHasCameraAutoMode autoCodec)) return; - - trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, autoCodec.CameraAutoModeIsOnFeedback.BoolValue); - trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !autoCodec.CameraAutoModeIsOnFeedback.BoolValue); - }; - - if (codec.CameraIsOffFeedback.BoolValue) + if (a.BoolValue) { trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true); trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false); @@ -699,822 +666,834 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); - if (!(codec is IHasCameraAutoMode autoModeCodec)) return; + if (!(codec is IHasCameraAutoMode autoCodec)) return; - trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, autoModeCodec.CameraAutoModeIsOnFeedback.BoolValue); - trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !autoModeCodec.CameraAutoModeIsOnFeedback.BoolValue); + trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, autoCodec.CameraAutoModeIsOnFeedback.BoolValue); + trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !autoCodec.CameraAutoModeIsOnFeedback.BoolValue); + }; + + if (codec.CameraIsOffFeedback.BoolValue) + { + trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true); + trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false); + trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false); + return; } - private void LinkVideoCodecVolumeToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMuteOn.JoinNumber]); - MuteFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.VolumeMuteOff.JoinNumber]); + trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); - trilist.SetSigFalseAction(joinMap.VolumeMuteOn.JoinNumber, MuteOn); - trilist.SetSigFalseAction(joinMap.VolumeMuteOff.JoinNumber, MuteOff); - trilist.SetSigFalseAction(joinMap.VolumeMuteToggle.JoinNumber, MuteToggle); - VolumeLevelFeedback.LinkInputSig(trilist.UShortInput[joinMap.VolumeLevel.JoinNumber]); + if (!(codec is IHasCameraAutoMode autoModeCodec)) return; - trilist.SetBoolSigAction(joinMap.VolumeUp.JoinNumber, VolumeUp); - trilist.SetBoolSigAction(joinMap.VolumeDown.JoinNumber, VolumeDown); + trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, autoModeCodec.CameraAutoModeIsOnFeedback.BoolValue); + trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !autoModeCodec.CameraAutoModeIsOnFeedback.BoolValue); + } - trilist.SetUShortSigAction(joinMap.VolumeLevel.JoinNumber, SetVolume); + private void LinkVideoCodecVolumeToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMuteOn.JoinNumber]); + MuteFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.VolumeMuteOff.JoinNumber]); - } + trilist.SetSigFalseAction(joinMap.VolumeMuteOn.JoinNumber, MuteOn); + trilist.SetSigFalseAction(joinMap.VolumeMuteOff.JoinNumber, MuteOff); + trilist.SetSigFalseAction(joinMap.VolumeMuteToggle.JoinNumber, MuteToggle); - private void LinkVideoCodecPrivacyToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - PrivacyModeIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.MicMuteOn.JoinNumber]); - PrivacyModeIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.MicMuteOff.JoinNumber]); + VolumeLevelFeedback.LinkInputSig(trilist.UShortInput[joinMap.VolumeLevel.JoinNumber]); - trilist.SetSigFalseAction(joinMap.MicMuteOn.JoinNumber, PrivacyModeOn); - trilist.SetSigFalseAction(joinMap.MicMuteOff.JoinNumber, PrivacyModeOff); - trilist.SetSigFalseAction(joinMap.MicMuteToggle.JoinNumber, PrivacyModeToggle); - } + trilist.SetBoolSigAction(joinMap.VolumeUp.JoinNumber, VolumeUp); + trilist.SetBoolSigAction(joinMap.VolumeDown.JoinNumber, VolumeDown); - private void LinkVideoCodecCommMonitorToApi(ICommunicationMonitor codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - codec.CommunicationMonitor.IsOnlineFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); - } + trilist.SetUShortSigAction(joinMap.VolumeLevel.JoinNumber, SetVolume); - private void LinkVideoCodecParticipantsToApi(IHasParticipants codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - // make sure to update the values when the EISC comes online - trilist.OnlineStatusChange += (sender, args) => - { - if (sender.IsOnline) - { - UpdateParticipantsXSig(codec, trilist, joinMap); - } - }; + } - // set actions and update the values when the list changes - codec.Participants.ParticipantsListHasChanged += (sender, args) => + private void LinkVideoCodecPrivacyToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + PrivacyModeIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.MicMuteOn.JoinNumber]); + PrivacyModeIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.MicMuteOff.JoinNumber]); + + trilist.SetSigFalseAction(joinMap.MicMuteOn.JoinNumber, PrivacyModeOn); + trilist.SetSigFalseAction(joinMap.MicMuteOff.JoinNumber, PrivacyModeOff); + trilist.SetSigFalseAction(joinMap.MicMuteToggle.JoinNumber, PrivacyModeToggle); + } + + private void LinkVideoCodecCommMonitorToApi(ICommunicationMonitor codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + codec.CommunicationMonitor.IsOnlineFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsOnline.JoinNumber]); + } + + private void LinkVideoCodecParticipantsToApi(IHasParticipants codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + // make sure to update the values when the EISC comes online + trilist.OnlineStatusChange += (sender, args) => { - SetParticipantActions(trilist, joinMap, codec.Participants.CurrentParticipants); - - UpdateParticipantsXSig(codec, trilist, joinMap); + if (sender.IsOnline) + { + UpdateParticipantsXSig(codec, trilist, joinMap); + } }; - trilist.OnlineStatusChange += (device, args) => - { - if (!args.DeviceOnLine) return; - - // TODO [ ] Issue #868 - trilist.SetString(joinMap.CurrentParticipants.JoinNumber, "\xFC"); - UpdateParticipantsXSig(codec, trilist, joinMap); - }; - } - - private void UpdateParticipantsXSig(IHasParticipants codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + // set actions and update the values when the list changes + codec.Participants.ParticipantsListHasChanged += (sender, args) => { - string participantsXSig; + SetParticipantActions(trilist, joinMap, codec.Participants.CurrentParticipants); - if (codec.Participants.CurrentParticipants.Count == 0) - { - participantsXSig = Encoding.GetEncoding(XSigEncoding).GetString(_clearBytes, 0, _clearBytes.Length); - trilist.SetString(joinMap.CurrentParticipants.JoinNumber, participantsXSig); - trilist.SetUshort(joinMap.ParticipantCount.JoinNumber, (ushort)codec.Participants.CurrentParticipants.Count); - return; - } + UpdateParticipantsXSig(codec, trilist, joinMap); + }; - participantsXSig = UpdateParticipantsXSig(codec.Participants.CurrentParticipants); + trilist.OnlineStatusChange += (device, args) => + { + if (!args.DeviceOnLine) return; + // TODO [ ] Issue #868 + trilist.SetString(joinMap.CurrentParticipants.JoinNumber, "\xFC"); + UpdateParticipantsXSig(codec, trilist, joinMap); + }; + } + + private void UpdateParticipantsXSig(IHasParticipants codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + string participantsXSig; + + if (codec.Participants.CurrentParticipants.Count == 0) + { + participantsXSig = Encoding.GetEncoding(XSigEncoding).GetString(_clearBytes, 0, _clearBytes.Length); trilist.SetString(joinMap.CurrentParticipants.JoinNumber, participantsXSig); - trilist.SetUshort(joinMap.ParticipantCount.JoinNumber, (ushort)codec.Participants.CurrentParticipants.Count); + return; } - /// - /// Sets the actions for each participant in the list - /// - private void SetParticipantActions(BasicTriList trilist, VideoCodecControllerJoinMap joinMap, List currentParticipants) + participantsXSig = UpdateParticipantsXSig(codec.Participants.CurrentParticipants); + + trilist.SetString(joinMap.CurrentParticipants.JoinNumber, participantsXSig); + + trilist.SetUshort(joinMap.ParticipantCount.JoinNumber, (ushort)codec.Participants.CurrentParticipants.Count); + } + + /// + /// Sets the actions for each participant in the list + /// + private void SetParticipantActions(BasicTriList trilist, VideoCodecControllerJoinMap joinMap, List currentParticipants) + { + uint index = 0; // track the index of the participant in the + + foreach (var participant in currentParticipants) { - uint index = 0; // track the index of the participant in the + var p = participant; + if (index > MaxParticipants) break; - foreach (var participant in currentParticipants) + if (this is IHasParticipantAudioMute audioMuteCodec) { - var p = participant; - if (index > MaxParticipants) break; + trilist.SetSigFalseAction(joinMap.ParticipantAudioMuteToggleStart.JoinNumber + index, + () => audioMuteCodec.ToggleAudioForParticipant(p.UserId)); - if (this is IHasParticipantAudioMute audioMuteCodec) - { - trilist.SetSigFalseAction(joinMap.ParticipantAudioMuteToggleStart.JoinNumber + index, - () => audioMuteCodec.ToggleAudioForParticipant(p.UserId)); - - trilist.SetSigFalseAction(joinMap.ParticipantVideoMuteToggleStart.JoinNumber + index, - () => audioMuteCodec.ToggleVideoForParticipant(p.UserId)); - } - - if (this is IHasParticipantPinUnpin pinCodec) - { - trilist.SetSigFalseAction(joinMap.ParticipantPinToggleStart.JoinNumber + index, - () => pinCodec.ToggleParticipantPinState(p.UserId, pinCodec.ScreenIndexToPinUserTo)); - } - - index++; + trilist.SetSigFalseAction(joinMap.ParticipantVideoMuteToggleStart.JoinNumber + index, + () => audioMuteCodec.ToggleVideoForParticipant(p.UserId)); } - // Clear out any previously set actions - while (index < MaxParticipants) + if (this is IHasParticipantPinUnpin pinCodec) { - trilist.ClearBoolSigAction(joinMap.ParticipantAudioMuteToggleStart.JoinNumber + index); - trilist.ClearBoolSigAction(joinMap.ParticipantVideoMuteToggleStart.JoinNumber + index); - trilist.ClearBoolSigAction(joinMap.ParticipantPinToggleStart.JoinNumber + index); - - index++; + trilist.SetSigFalseAction(joinMap.ParticipantPinToggleStart.JoinNumber + index, + () => pinCodec.ToggleParticipantPinState(p.UserId, pinCodec.ScreenIndexToPinUserTo)); } + + index++; } - private string UpdateParticipantsXSig(List currentParticipants) + // Clear out any previously set actions + while (index < MaxParticipants) { - const int maxParticipants = MaxParticipants; - const int maxDigitals = 7; - const int maxStrings = 1; - const int maxAnalogs = 1; - const int offset = maxDigitals + maxStrings + maxAnalogs; // 9 - var digitalIndex = (maxStrings + maxAnalogs) * maxParticipants; // 100 - var stringIndex = 0; - var analogIndex = stringIndex + maxParticipants; - var meetingIndex = 0; + trilist.ClearBoolSigAction(joinMap.ParticipantAudioMuteToggleStart.JoinNumber + index); + trilist.ClearBoolSigAction(joinMap.ParticipantVideoMuteToggleStart.JoinNumber + index); + trilist.ClearBoolSigAction(joinMap.ParticipantPinToggleStart.JoinNumber + index); - var tokenArray = new XSigToken[maxParticipants * offset]; + index++; + } + } - foreach (var participant in currentParticipants) - { - if (meetingIndex >= maxParticipants * offset) break; + private string UpdateParticipantsXSig(List currentParticipants) + { + const int maxParticipants = MaxParticipants; + const int maxDigitals = 7; + const int maxStrings = 1; + const int maxAnalogs = 1; + const int offset = maxDigitals + maxStrings + maxAnalogs; // 9 + var digitalIndex = (maxStrings + maxAnalogs) * maxParticipants; // 100 + var stringIndex = 0; + var analogIndex = stringIndex + maxParticipants; + var meetingIndex = 0; - // Debug.LogMessage(LogEventLevel.Verbose, this, - //@"Updating Participant on xsig: - //Name: {0} (s{9}) - //AudioMute: {1} (d{10}) - //VideoMute: {2} (d{11}) - //CanMuteVideo: {3} (d{12}) - //CanUMuteVideo: {4} (d{13}) - //IsHost: {5} (d{14}) - //HandIsRaised: {6} (d{15}) - //IsPinned: {7} (d{16}) - //ScreenIndexIsPinnedTo: {8} (a{17}) - //", - // participant.Name, - // participant.AudioMuteFb, - // participant.VideoMuteFb, - // participant.CanMuteVideo, - // participant.CanUnmuteVideo, - // participant.IsHost, - // participant.HandIsRaisedFb, - // participant.IsPinnedFb, - // participant.ScreenIndexIsPinnedToFb, - // stringIndex + 1, - // digitalIndex + 1, - // digitalIndex + 2, - // digitalIndex + 3, - // digitalIndex + 4, - // digitalIndex + 5, - // digitalIndex + 6, - // digitalIndex + 7, - // analogIndex + 1 - // ); + var tokenArray = new XSigToken[maxParticipants * offset]; + + foreach (var participant in currentParticipants) + { + if (meetingIndex >= maxParticipants * offset) break; + + // Debug.LogMessage(LogEventLevel.Verbose, this, + //@"Updating Participant on xsig: + //Name: {0} (s{9}) + //AudioMute: {1} (d{10}) + //VideoMute: {2} (d{11}) + //CanMuteVideo: {3} (d{12}) + //CanUMuteVideo: {4} (d{13}) + //IsHost: {5} (d{14}) + //HandIsRaised: {6} (d{15}) + //IsPinned: {7} (d{16}) + //ScreenIndexIsPinnedTo: {8} (a{17}) + //", + // participant.Name, + // participant.AudioMuteFb, + // participant.VideoMuteFb, + // participant.CanMuteVideo, + // participant.CanUnmuteVideo, + // participant.IsHost, + // participant.HandIsRaisedFb, + // participant.IsPinnedFb, + // participant.ScreenIndexIsPinnedToFb, + // stringIndex + 1, + // digitalIndex + 1, + // digitalIndex + 2, + // digitalIndex + 3, + // digitalIndex + 4, + // digitalIndex + 5, + // digitalIndex + 6, + // digitalIndex + 7, + // analogIndex + 1 + // ); - //digitals - tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, participant.AudioMuteFb); - tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, participant.VideoMuteFb); - tokenArray[digitalIndex + 2] = new XSigDigitalToken(digitalIndex + 3, participant.CanMuteVideo); - tokenArray[digitalIndex + 3] = new XSigDigitalToken(digitalIndex + 4, participant.CanUnmuteVideo); - tokenArray[digitalIndex + 4] = new XSigDigitalToken(digitalIndex + 5, participant.IsHost); - tokenArray[digitalIndex + 5] = new XSigDigitalToken(digitalIndex + 6, participant.HandIsRaisedFb); - tokenArray[digitalIndex + 6] = new XSigDigitalToken(digitalIndex + 7, participant.IsPinnedFb); + //digitals + tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, participant.AudioMuteFb); + tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, participant.VideoMuteFb); + tokenArray[digitalIndex + 2] = new XSigDigitalToken(digitalIndex + 3, participant.CanMuteVideo); + tokenArray[digitalIndex + 3] = new XSigDigitalToken(digitalIndex + 4, participant.CanUnmuteVideo); + tokenArray[digitalIndex + 4] = new XSigDigitalToken(digitalIndex + 5, participant.IsHost); + tokenArray[digitalIndex + 5] = new XSigDigitalToken(digitalIndex + 6, participant.HandIsRaisedFb); + tokenArray[digitalIndex + 6] = new XSigDigitalToken(digitalIndex + 7, participant.IsPinnedFb); - Debug.LogMessage(LogEventLevel.Verbose, this, "Index: {0} byte value: {1}", digitalIndex + 7, ComTextHelper.GetEscapedText(tokenArray[digitalIndex + 6].GetBytes())); + Debug.LogMessage(LogEventLevel.Verbose, this, "Index: {0} byte value: {1}", digitalIndex + 7, ComTextHelper.GetEscapedText(tokenArray[digitalIndex + 6].GetBytes())); - //serials - tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, participant.Name); + //serials + tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, participant.Name); - //analogs - tokenArray[analogIndex] = new XSigAnalogToken(analogIndex + 1, (ushort)participant.ScreenIndexIsPinnedToFb); + //analogs + tokenArray[analogIndex] = new XSigAnalogToken(analogIndex + 1, (ushort)participant.ScreenIndexIsPinnedToFb); - digitalIndex += maxDigitals; - meetingIndex += offset; - stringIndex += maxStrings; - analogIndex += maxAnalogs; - } - - while (meetingIndex < maxParticipants * offset) - { - //digitals - tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, false); - tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, false); - tokenArray[digitalIndex + 2] = new XSigDigitalToken(digitalIndex + 3, false); - tokenArray[digitalIndex + 3] = new XSigDigitalToken(digitalIndex + 4, false); - tokenArray[digitalIndex + 4] = new XSigDigitalToken(digitalIndex + 5, false); - tokenArray[digitalIndex + 5] = new XSigDigitalToken(digitalIndex + 6, false); - tokenArray[digitalIndex + 6] = new XSigDigitalToken(digitalIndex + 7, false); - - //serials - tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, String.Empty); - - //analogs - tokenArray[analogIndex] = new XSigAnalogToken(analogIndex + 1, 0); - - digitalIndex += maxDigitals; - meetingIndex += offset; - stringIndex += maxStrings; - analogIndex += maxAnalogs; - } - - var returnString = GetXSigString(tokenArray); - - //Debug.LogMessage(LogEventLevel.Verbose, this, "{0}", ComTextHelper.GetEscapedText(Encoding.GetEncoding(28591).GetBytes(returnString))); - - - return returnString; + digitalIndex += maxDigitals; + meetingIndex += offset; + stringIndex += maxStrings; + analogIndex += maxAnalogs; } - private void LinkVideoCodecContentSharingToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + while (meetingIndex < maxParticipants * offset) { - SharingContentIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.SourceShareStart.JoinNumber]); - SharingContentIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.SourceShareEnd.JoinNumber]); + //digitals + tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, false); + tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, false); + tokenArray[digitalIndex + 2] = new XSigDigitalToken(digitalIndex + 3, false); + tokenArray[digitalIndex + 3] = new XSigDigitalToken(digitalIndex + 4, false); + tokenArray[digitalIndex + 4] = new XSigDigitalToken(digitalIndex + 5, false); + tokenArray[digitalIndex + 5] = new XSigDigitalToken(digitalIndex + 6, false); + tokenArray[digitalIndex + 6] = new XSigDigitalToken(digitalIndex + 7, false); - SharingSourceFeedback.LinkInputSig(trilist.StringInput[joinMap.CurrentSource.JoinNumber]); + //serials + tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, String.Empty); - trilist.SetSigFalseAction(joinMap.SourceShareStart.JoinNumber, StartSharing); - trilist.SetSigFalseAction(joinMap.SourceShareEnd.JoinNumber, StopSharing); + //analogs + tokenArray[analogIndex] = new XSigAnalogToken(analogIndex + 1, 0); - trilist.SetBoolSigAction(joinMap.SourceShareAutoStart.JoinNumber, b => AutoShareContentWhileInCall = b); + digitalIndex += maxDigitals; + meetingIndex += offset; + stringIndex += maxStrings; + analogIndex += maxAnalogs; } - private List _currentMeetings = new List(); + var returnString = GetXSigString(tokenArray); - private void LinkVideoCodecScheduleToApi(IHasScheduleAwareness codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + //Debug.LogMessage(LogEventLevel.Verbose, this, "{0}", ComTextHelper.GetEscapedText(Encoding.GetEncoding(28591).GetBytes(returnString))); + + + return returnString; + } + + private void LinkVideoCodecContentSharingToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + SharingContentIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.SourceShareStart.JoinNumber]); + SharingContentIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.SourceShareEnd.JoinNumber]); + + SharingSourceFeedback.LinkInputSig(trilist.StringInput[joinMap.CurrentSource.JoinNumber]); + + trilist.SetSigFalseAction(joinMap.SourceShareStart.JoinNumber, StartSharing); + trilist.SetSigFalseAction(joinMap.SourceShareEnd.JoinNumber, StopSharing); + + trilist.SetBoolSigAction(joinMap.SourceShareAutoStart.JoinNumber, b => AutoShareContentWhileInCall = b); + } + + private List _currentMeetings = new List(); + + private void LinkVideoCodecScheduleToApi(IHasScheduleAwareness codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + trilist.SetSigFalseAction(joinMap.UpdateMeetings.JoinNumber, codec.GetSchedule); + + trilist.SetUShortSigAction(joinMap.MinutesBeforeMeetingStart.JoinNumber, (i) => { - trilist.SetSigFalseAction(joinMap.UpdateMeetings.JoinNumber, codec.GetSchedule); + codec.CodecSchedule.MeetingWarningMinutes = i; + }); - trilist.SetUShortSigAction(joinMap.MinutesBeforeMeetingStart.JoinNumber, (i) => + + for (uint i = 0; i < joinMap.DialMeetingStart.JoinSpan; i++) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Setting action to Dial Meeting {0} to digital join {1}", i + 1, joinMap.DialMeetingStart.JoinNumber + i); + var joinNumber = joinMap.DialMeetingStart.JoinNumber + i; + var mtg = i + 1; + var index = (int)i; + + trilist.SetSigFalseAction(joinNumber, () => { - codec.CodecSchedule.MeetingWarningMinutes = i; + Debug.LogMessage(LogEventLevel.Debug, this, "Meeting {0} Selected (EISC dig-o{1}) > _currentMeetings[{2}].Id: {3}, Title: {4}", + mtg, joinMap.DialMeetingStart.JoinNumber + i, index, _currentMeetings[index].Id, _currentMeetings[index].Title); + if (_currentMeetings[index] != null) + Dial(_currentMeetings[index]); }); - - - for (uint i = 0; i < joinMap.DialMeetingStart.JoinSpan; i++) - { - Debug.LogMessage(LogEventLevel.Debug, this, "Setting action to Dial Meeting {0} to digital join {1}", i + 1, joinMap.DialMeetingStart.JoinNumber + i); - var joinNumber = joinMap.DialMeetingStart.JoinNumber + i; - var mtg = i + 1; - var index = (int)i; - - trilist.SetSigFalseAction(joinNumber, () => - { - Debug.LogMessage(LogEventLevel.Debug, this, "Meeting {0} Selected (EISC dig-o{1}) > _currentMeetings[{2}].Id: {3}, Title: {4}", - mtg, joinMap.DialMeetingStart.JoinNumber + i, index, _currentMeetings[index].Id, _currentMeetings[index].Title); - if (_currentMeetings[index] != null) - Dial(_currentMeetings[index]); - }); - } - - codec.CodecSchedule.MeetingsListHasChanged += (sender, args) => UpdateMeetingsList(codec, trilist, joinMap); - codec.CodecSchedule.MeetingEventChange += (sender, args) => - { - if (args.ChangeType == eMeetingEventChangeType.MeetingStartWarning) - { - UpdateMeetingsList(codec, trilist, joinMap); - } - }; - - trilist.SetUShortSigAction(joinMap.MeetingsToDisplay.JoinNumber, m => MeetingsToDisplay = m); - MeetingsToDisplayFeedback.LinkInputSig(trilist.UShortInput[joinMap.MeetingsToDisplay.JoinNumber]); - - trilist.OnlineStatusChange += (device, args) => - { - if (!args.DeviceOnLine) return; - - // TODO [ ] Issue #868 - trilist.SetString(joinMap.Schedule.JoinNumber, "\xFC"); - UpdateMeetingsList(codec, trilist, joinMap); - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set - MeetingsToDisplayFeedback.LinkInputSig(trilist.UShortInput[joinMap.MeetingsToDisplay.JoinNumber]); - }; } - private void UpdateMeetingsList(IHasScheduleAwareness codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + codec.CodecSchedule.MeetingsListHasChanged += (sender, args) => UpdateMeetingsList(codec, trilist, joinMap); + codec.CodecSchedule.MeetingEventChange += (sender, args) => + { + if (args.ChangeType == eMeetingEventChangeType.MeetingStartWarning) + { + UpdateMeetingsList(codec, trilist, joinMap); + } + }; + + trilist.SetUShortSigAction(joinMap.MeetingsToDisplay.JoinNumber, m => MeetingsToDisplay = m); + MeetingsToDisplayFeedback.LinkInputSig(trilist.UShortInput[joinMap.MeetingsToDisplay.JoinNumber]); + + trilist.OnlineStatusChange += (device, args) => + { + if (!args.DeviceOnLine) return; + + // TODO [ ] Issue #868 + trilist.SetString(joinMap.Schedule.JoinNumber, "\xFC"); + UpdateMeetingsList(codec, trilist, joinMap); + // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + MeetingsToDisplayFeedback.LinkInputSig(trilist.UShortInput[joinMap.MeetingsToDisplay.JoinNumber]); + }; + } + + private void UpdateMeetingsList(IHasScheduleAwareness codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + var currentTime = DateTime.Now; + + _currentMeetings = codec.CodecSchedule.Meetings.Where(m => m.StartTime >= currentTime || m.EndTime >= currentTime).ToList(); + + if (_currentMeetings.Count == 0) + { + var emptyXSigByteArray = XSigHelpers.ClearOutputs(); + var emptyXSigString = Encoding.GetEncoding(XSigEncoding) + .GetString(emptyXSigByteArray, 0, emptyXSigByteArray.Length); + + trilist.SetString(joinMap.Schedule.JoinNumber, emptyXSigString); + return; + } + + var meetingsData = UpdateMeetingsListXSig(_currentMeetings); + trilist.SetString(joinMap.Schedule.JoinNumber, meetingsData); + trilist.SetUshort(joinMap.MeetingCount.JoinNumber, (ushort)_currentMeetings.Count); + + trilist.OnlineStatusChange += (device, args) => + { + if (!args.DeviceOnLine) return; + + // TODO [ ] Issue #868 + trilist.SetString(joinMap.Schedule.JoinNumber, "\xFC"); + UpdateMeetingsListXSig(_currentMeetings); + }; + } + + + // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + private int _meetingsToDisplay = 3; + // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + protected int MeetingsToDisplay + { + get { return _meetingsToDisplay; } + set + { + _meetingsToDisplay = (ushort)(value == 0 ? 3 : value); + MeetingsToDisplayFeedback.FireUpdate(); + } + } + + // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + public IntFeedback MeetingsToDisplayFeedback { get; set; } + + private string UpdateMeetingsListXSig(List meetings) + { + // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + //const int _meetingsToDisplay = 3; + const int maxDigitals = 2; + const int maxStrings = 7; + const int offset = maxDigitals + maxStrings; + var digitalIndex = maxStrings * _meetingsToDisplay; //15 + var stringIndex = 0; + var meetingIndex = 0; + + var tokenArray = new XSigToken[_meetingsToDisplay * offset]; + /* + * Digitals + * IsJoinable - 1 + * IsDialable - 2 + * + * Serials + * Organizer - 1 + * Title - 2 + * Start Date - 3 + * Start Time - 4 + * End Date - 5 + * End Time - 6 + * Id - 7 + */ + + + foreach (var meeting in meetings) { var currentTime = DateTime.Now; - _currentMeetings = codec.CodecSchedule.Meetings.Where(m => m.StartTime >= currentTime || m.EndTime >= currentTime).ToList(); + if (meeting.StartTime < currentTime && meeting.EndTime < currentTime) continue; - if (_currentMeetings.Count == 0) + if (meetingIndex >= _meetingsToDisplay * offset) { - var emptyXSigByteArray = XSigHelpers.ClearOutputs(); - var emptyXSigString = Encoding.GetEncoding(XSigEncoding) - .GetString(emptyXSigByteArray, 0, emptyXSigByteArray.Length); + Debug.LogMessage(LogEventLevel.Verbose, this, "Max Meetings reached"); + break; + } - trilist.SetString(joinMap.Schedule.JoinNumber, emptyXSigString); + //digitals + tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, meeting.Joinable); + tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, meeting.Dialable); + + //serials + tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, meeting.Organizer); + tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, meeting.Title); + tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, meeting.StartTime.ToString(string.IsNullOrEmpty(_dateFormatSpecifier) ? "d" : _dateFormatSpecifier, Global.Culture)); + tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, meeting.StartTime.ToString(string.IsNullOrEmpty(_timeFormatSpecifier) ? "t" : _timeFormatSpecifier, Global.Culture)); + tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, meeting.EndTime.ToString(string.IsNullOrEmpty(_dateFormatSpecifier) ? "d" : _dateFormatSpecifier, Global.Culture)); + tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, meeting.EndTime.ToString(string.IsNullOrEmpty(_timeFormatSpecifier) ? "t" : _timeFormatSpecifier, Global.Culture)); + tokenArray[stringIndex + 6] = new XSigSerialToken(stringIndex + 7, meeting.Id); + + digitalIndex += maxDigitals; + meetingIndex += offset; + stringIndex += maxStrings; + } + + while (meetingIndex < _meetingsToDisplay * offset) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Clearing unused data. Meeting Index: {0} MaxMeetings * Offset: {1}", + meetingIndex, _meetingsToDisplay * offset); + + //digitals + tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, false); + tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, false); + + //serials + tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, String.Empty); + tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, String.Empty); + tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, String.Empty); + tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, String.Empty); + tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, String.Empty); + tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, String.Empty); + tokenArray[stringIndex + 6] = new XSigSerialToken(stringIndex + 7, String.Empty); + + digitalIndex += maxDigitals; + meetingIndex += offset; + stringIndex += maxStrings; + } + + return GetXSigString(tokenArray); + } + + + private void LinkVideoCodecDirectoryToApi(IHasDirectory codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + codec.CurrentDirectoryResultIsNotDirectoryRoot.LinkComplementInputSig( + trilist.BooleanInput[joinMap.DirectoryIsRoot.JoinNumber]); + + trilist.SetStringSigAction(joinMap.DirectorySearchString.JoinNumber, codec.SearchDirectory); + + trilist.SetUShortSigAction(joinMap.DirectorySelectRow.JoinNumber, (i) => SelectDirectoryEntry(codec, i, trilist, joinMap)); + + //Special Change for protected directory clear + + trilist.SetBoolSigAction(joinMap.DirectoryClearSelected.JoinNumber, (b) => SelectDirectoryEntry(_directoryCodec, 0, _directoryTrilist, _directoryJoinmap)); + + // Report feedback for number of contact methods for selected contact + + trilist.SetSigFalseAction(joinMap.DirectoryRoot.JoinNumber, codec.SetCurrentDirectoryToRoot); + + trilist.SetSigFalseAction(joinMap.DirectoryFolderBack.JoinNumber, codec.GetDirectoryParentFolderContents); + + if (codec.DirectoryRoot != null) + { + var contactsCount = codec.DirectoryRoot.CurrentDirectoryResults.Where(c => c.ParentFolderId.Equals("root")).ToList().Count; + trilist.SetUshort(joinMap.DirectoryRowCount.JoinNumber, (ushort)contactsCount); + Debug.LogMessage(LogEventLevel.Verbose, this, ">>> contactsCount: {0}", contactsCount); + + var clearBytes = XSigHelpers.ClearOutputs(); + + trilist.SetString(joinMap.DirectoryEntries.JoinNumber, + Encoding.GetEncoding(XSigEncoding).GetString(clearBytes, 0, clearBytes.Length)); + var directoryXSig = UpdateDirectoryXSig(codec.DirectoryRoot, + codec.CurrentDirectoryResultIsNotDirectoryRoot.BoolValue == false); + + Debug.LogMessage(LogEventLevel.Verbose, this, "Directory XSig Length: {0}", directoryXSig.Length); + + trilist.SetString(joinMap.DirectoryEntries.JoinNumber, directoryXSig); + } + + codec.DirectoryResultReturned += (sender, args) => + { + var isRoot = codec.CurrentDirectoryResultIsNotDirectoryRoot.BoolValue == false; + var argsCount = isRoot + ? args.Directory.CurrentDirectoryResults.Where(a => a.ParentFolderId.Equals("root")).ToList().Count + : args.Directory.CurrentDirectoryResults.Count; + + trilist.SetUshort(joinMap.DirectoryRowCount.JoinNumber, (ushort)argsCount); + Debug.LogMessage(LogEventLevel.Verbose, this, ">>> argsCount: {0}", argsCount); + + var clearBytes = XSigHelpers.ClearOutputs(); + + trilist.SetString(joinMap.DirectoryEntries.JoinNumber, + Encoding.GetEncoding(XSigEncoding).GetString(clearBytes, 0, clearBytes.Length)); + var directoryXSig = UpdateDirectoryXSig(args.Directory, + codec.CurrentDirectoryResultIsNotDirectoryRoot.BoolValue == false); + Debug.LogMessage(LogEventLevel.Verbose, this, "Directory XSig Length: {0}", directoryXSig.Length); + + trilist.SetString(joinMap.DirectoryEntries.JoinNumber, directoryXSig); + }; + + trilist.OnlineStatusChange += (device, args) => + { + if (!args.DeviceOnLine) return; + + var clearBytes = XSigHelpers.ClearOutputs(); + trilist.SetString(joinMap.DirectoryEntries.JoinNumber, + Encoding.GetEncoding(XSigEncoding).GetString(clearBytes, 0, clearBytes.Length)); + var directoryXSig = UpdateDirectoryXSig(codec.DirectoryRoot, codec.CurrentDirectoryResultIsNotDirectoryRoot.BoolValue == false); + trilist.SetString(joinMap.DirectoryEntries.JoinNumber, directoryXSig); + }; + } + + private void SelectDirectoryEntry(IHasDirectory codec, ushort i, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + if (i > codec.CurrentDirectoryResult.CurrentDirectoryResults.Count) return; + _selectedDirectoryItem = i == 0 ? null : codec.CurrentDirectoryResult.CurrentDirectoryResults[i - 1]; + trilist.SetUshort(joinMap.DirectorySelectRowFeedback.JoinNumber, i); + + if (_selectedDirectoryItem == null) trilist.SetBool(joinMap.DirectoryEntryIsContact.JoinNumber, false); + + + if (_selectedDirectoryItem is DirectoryFolder) + { + codec.GetDirectoryFolderContents(_selectedDirectoryItem.FolderId); + trilist.SetUshort(joinMap.SelectedContactMethodCount.JoinNumber, 0); + trilist.SetString(joinMap.DirectorySelectedFolderName.JoinNumber, _selectedDirectoryItem.Name); + trilist.SetString(joinMap.DirectoryEntrySelectedName.JoinNumber, string.Empty); + trilist.ClearUShortSigAction(joinMap.SelectContactMethod.JoinNumber); + trilist.ClearBoolSigAction(joinMap.DirectoryDialSelectedLine.JoinNumber); + trilist.ClearBoolSigAction(joinMap.DirectoryDialSelectedContactMethod.JoinNumber); + trilist.SetBool(joinMap.DirectoryEntryIsContact.JoinNumber, false); + return; + } + + // not a folder. Clear this value + trilist.SetString(joinMap.DirectorySelectedFolderName.JoinNumber, string.Empty); + + var selectedContact = _selectedDirectoryItem as DirectoryContact; + + if (selectedContact != null && selectedContact.ContactMethods.Count >= 1) + { + trilist.SetBool(joinMap.DirectoryEntryIsContact.JoinNumber, true); + } + + trilist.SetString(joinMap.DirectoryEntrySelectedName.JoinNumber, + selectedContact != null ? selectedContact.Name : string.Empty); + + // Allow auto dial of selected line. Always dials first contact method + if (!trilist.GetBool(joinMap.DirectoryDisableAutoDialSelectedLine.JoinNumber)) + { + if (_selectedDirectoryItem is IInvitableContact invitableEntry) + { + Dial(invitableEntry); return; } - var meetingsData = UpdateMeetingsListXSig(_currentMeetings); - trilist.SetString(joinMap.Schedule.JoinNumber, meetingsData); - trilist.SetUshort(joinMap.MeetingCount.JoinNumber, (ushort)_currentMeetings.Count); - trilist.OnlineStatusChange += (device, args) => - { - if (!args.DeviceOnLine) return; + trilist.SetString(joinMap.DirectoryEntrySelectedNumber.JoinNumber, + selectedContact != null ? selectedContact.ContactMethods[0].Number : string.Empty); - // TODO [ ] Issue #868 - trilist.SetString(joinMap.Schedule.JoinNumber, "\xFC"); - UpdateMeetingsListXSig(_currentMeetings); - }; + if (!(_selectedDirectoryItem is DirectoryContact entryToDial)) return; + + Dial(entryToDial.ContactMethods[0].Number); } - - - - private int _meetingsToDisplay = 3; - - /// - /// Gets or sets the number of meetings to display on the XSIG - /// - protected int MeetingsToDisplay + else { - get { return _meetingsToDisplay; } - set + // If auto dial is disabled... + + if (!(_selectedDirectoryItem is DirectoryContact entryToDial)) { - _meetingsToDisplay = (ushort)(value == 0 ? 3 : value); - MeetingsToDisplayFeedback.FireUpdate(); - } - } - - /// - /// Gets or sets the MeetingsToDisplayFeedback - /// - public IntFeedback MeetingsToDisplayFeedback { get; set; } - - private string UpdateMeetingsListXSig(List meetings) - { - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set - //const int _meetingsToDisplay = 3; - const int maxDigitals = 2; - const int maxStrings = 7; - const int offset = maxDigitals + maxStrings; - var digitalIndex = maxStrings * _meetingsToDisplay; //15 - var stringIndex = 0; - var meetingIndex = 0; - - var tokenArray = new XSigToken[_meetingsToDisplay * offset]; - /* - * Digitals - * IsJoinable - 1 - * IsDialable - 2 - * - * Serials - * Organizer - 1 - * Title - 2 - * Start Date - 3 - * Start Time - 4 - * End Date - 5 - * End Time - 6 - * Id - 7 - */ - - - foreach (var meeting in meetings) - { - var currentTime = DateTime.Now; - - if (meeting.StartTime < currentTime && meeting.EndTime < currentTime) continue; - - if (meetingIndex >= _meetingsToDisplay * offset) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Max Meetings reached"); - break; - } - - //digitals - tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, meeting.Joinable); - tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, meeting.Dialable); - - //serials - tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, meeting.Organizer); - tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, meeting.Title); - tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, meeting.StartTime.ToString(_dateFormatSpecifier.NullIfEmpty() ?? "d", Global.Culture)); - tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, meeting.StartTime.ToString(_timeFormatSpecifier.NullIfEmpty() ?? "t", Global.Culture)); - tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, meeting.EndTime.ToString(_dateFormatSpecifier.NullIfEmpty() ?? "d", Global.Culture)); - tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, meeting.EndTime.ToString(_timeFormatSpecifier.NullIfEmpty() ?? "t", Global.Culture)); - tokenArray[stringIndex + 6] = new XSigSerialToken(stringIndex + 7, meeting.Id); - - digitalIndex += maxDigitals; - meetingIndex += offset; - stringIndex += maxStrings; - } - - while (meetingIndex < _meetingsToDisplay * offset) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Clearing unused data. Meeting Index: {0} MaxMeetings * Offset: {1}", - meetingIndex, _meetingsToDisplay * offset); - - //digitals - tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, false); - tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, false); - - //serials - tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, String.Empty); - tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, String.Empty); - tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, String.Empty); - tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, String.Empty); - tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, String.Empty); - tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, String.Empty); - tokenArray[stringIndex + 6] = new XSigSerialToken(stringIndex + 7, String.Empty); - - digitalIndex += maxDigitals; - meetingIndex += offset; - stringIndex += maxStrings; - } - - return GetXSigString(tokenArray); - } - - - private void LinkVideoCodecDirectoryToApi(IHasDirectory codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - codec.CurrentDirectoryResultIsNotDirectoryRoot.LinkComplementInputSig( - trilist.BooleanInput[joinMap.DirectoryIsRoot.JoinNumber]); - - trilist.SetStringSigAction(joinMap.DirectorySearchString.JoinNumber, codec.SearchDirectory); - - trilist.SetUShortSigAction(joinMap.DirectorySelectRow.JoinNumber, (i) => SelectDirectoryEntry(codec, i, trilist, joinMap)); - - //Special Change for protected directory clear - - trilist.SetBoolSigAction(joinMap.DirectoryClearSelected.JoinNumber, (b) => SelectDirectoryEntry(codec, 0, trilist, joinMap)); - - // Report feedback for number of contact methods for selected contact - - trilist.SetSigFalseAction(joinMap.DirectoryRoot.JoinNumber, codec.SetCurrentDirectoryToRoot); - - trilist.SetSigFalseAction(joinMap.DirectoryFolderBack.JoinNumber, codec.GetDirectoryParentFolderContents); - - if (codec.DirectoryRoot != null) - { - var contactsCount = codec.DirectoryRoot.CurrentDirectoryResults.Where(c => c.ParentFolderId.Equals("root")).ToList().Count; - trilist.SetUshort(joinMap.DirectoryRowCount.JoinNumber, (ushort)contactsCount); - Debug.LogMessage(LogEventLevel.Verbose, this, ">>> contactsCount: {0}", contactsCount); - - var clearBytes = XSigHelpers.ClearOutputs(); - - trilist.SetString(joinMap.DirectoryEntries.JoinNumber, - Encoding.GetEncoding(XSigEncoding).GetString(clearBytes, 0, clearBytes.Length)); - var directoryXSig = UpdateDirectoryXSig(codec.DirectoryRoot, - codec.CurrentDirectoryResultIsNotDirectoryRoot.BoolValue == false); - - Debug.LogMessage(LogEventLevel.Verbose, this, "Directory XSig Length: {0}", directoryXSig.Length); - - trilist.SetString(joinMap.DirectoryEntries.JoinNumber, directoryXSig); - } - - codec.DirectoryResultReturned += (sender, args) => - { - var isRoot = codec.CurrentDirectoryResultIsNotDirectoryRoot.BoolValue == false; - var argsCount = isRoot - ? args.Directory.CurrentDirectoryResults.Where(a => a.ParentFolderId.Equals("root")).ToList().Count - : args.Directory.CurrentDirectoryResults.Count; - - trilist.SetUshort(joinMap.DirectoryRowCount.JoinNumber, (ushort)argsCount); - Debug.LogMessage(LogEventLevel.Verbose, this, ">>> argsCount: {0}", argsCount); - - var clearBytes = XSigHelpers.ClearOutputs(); - - trilist.SetString(joinMap.DirectoryEntries.JoinNumber, - Encoding.GetEncoding(XSigEncoding).GetString(clearBytes, 0, clearBytes.Length)); - var directoryXSig = UpdateDirectoryXSig(args.Directory, - codec.CurrentDirectoryResultIsNotDirectoryRoot.BoolValue == false); - Debug.LogMessage(LogEventLevel.Verbose, this, "Directory XSig Length: {0}", directoryXSig.Length); - - trilist.SetString(joinMap.DirectoryEntries.JoinNumber, directoryXSig); - }; - - trilist.OnlineStatusChange += (device, args) => - { - if (!args.DeviceOnLine) return; - - var clearBytes = XSigHelpers.ClearOutputs(); - trilist.SetString(joinMap.DirectoryEntries.JoinNumber, - Encoding.GetEncoding(XSigEncoding).GetString(clearBytes, 0, clearBytes.Length)); - var directoryXSig = UpdateDirectoryXSig(codec.DirectoryRoot, codec.CurrentDirectoryResultIsNotDirectoryRoot.BoolValue == false); - trilist.SetString(joinMap.DirectoryEntries.JoinNumber, directoryXSig); - }; - } - - private void SelectDirectoryEntry(IHasDirectory codec, ushort i, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - if (i > codec.CurrentDirectoryResult.CurrentDirectoryResults.Count) return; - _selectedDirectoryItem = i == 0 ? null : codec.CurrentDirectoryResult.CurrentDirectoryResults[i - 1]; - trilist.SetUshort(joinMap.DirectorySelectRowFeedback.JoinNumber, i); - - if (_selectedDirectoryItem == null) trilist.SetBool(joinMap.DirectoryEntryIsContact.JoinNumber, false); - - - if (_selectedDirectoryItem is DirectoryFolder) - { - codec.GetDirectoryFolderContents(_selectedDirectoryItem.FolderId); + // Clear out values and actions from last selected item trilist.SetUshort(joinMap.SelectedContactMethodCount.JoinNumber, 0); - trilist.SetString(joinMap.DirectorySelectedFolderName.JoinNumber, _selectedDirectoryItem.Name); trilist.SetString(joinMap.DirectoryEntrySelectedName.JoinNumber, string.Empty); trilist.ClearUShortSigAction(joinMap.SelectContactMethod.JoinNumber); trilist.ClearBoolSigAction(joinMap.DirectoryDialSelectedLine.JoinNumber); trilist.ClearBoolSigAction(joinMap.DirectoryDialSelectedContactMethod.JoinNumber); - trilist.SetBool(joinMap.DirectoryEntryIsContact.JoinNumber, false); return; } - // not a folder. Clear this value - trilist.SetString(joinMap.DirectorySelectedFolderName.JoinNumber, string.Empty); + trilist.SetUshort(joinMap.SelectedContactMethodCount.JoinNumber, (ushort)entryToDial.ContactMethods.Count); - var selectedContact = _selectedDirectoryItem as DirectoryContact; - - if (selectedContact != null && selectedContact.ContactMethods.Count >= 1) + // Update the action to dial the selected contact method + trilist.SetUShortSigAction(joinMap.SelectContactMethod.JoinNumber, (u) => { - trilist.SetBool(joinMap.DirectoryEntryIsContact.JoinNumber, true); + if (u < 1 || u > entryToDial.ContactMethods.Count) return; + + trilist.SetSigFalseAction(joinMap.DirectoryDialSelectedContactMethod.JoinNumber, () => Dial(entryToDial.ContactMethods[u - 1].Number)); + }); + + // Sets DirectoryDialSelectedLine join action to dial first contact method + trilist.SetSigFalseAction(joinMap.DirectoryDialSelectedLine.JoinNumber, () => Dial(entryToDial.ContactMethods[0].Number)); + + var clearBytes = XSigHelpers.ClearOutputs(); + + trilist.SetString(joinMap.ContactMethods.JoinNumber, + Encoding.GetEncoding(XSigEncoding).GetString(clearBytes, 0, clearBytes.Length)); + var contactMethodsXSig = UpdateContactMethodsXSig(entryToDial); + + trilist.SetString(joinMap.ContactMethods.JoinNumber, contactMethodsXSig); + } + } + + /// + /// Generates the XSig data representing the available contact methods for the selected DirectoryContact + /// + /// + /// + private string UpdateContactMethodsXSig(DirectoryContact contact) + { + const int maxMethods = 10; + const int maxStrings = 3; + const int offset = maxStrings; + var stringIndex = 0; + var arrayIndex = 0; + // Create a new token array and set the size to the number of methods times the total number of signals + var tokenArray = new XSigToken[maxMethods * offset]; + + Debug.LogMessage(LogEventLevel.Verbose, this, "Creating XSIG token array with size {0}", maxMethods * offset); + + // TODO: Add code to generate XSig data + foreach (var method in contact.ContactMethods) + { + if (arrayIndex >= maxMethods * offset) + break; + + //serials + tokenArray[arrayIndex + 1] = new XSigSerialToken(stringIndex + 1, method.Number); + tokenArray[arrayIndex + 2] = new XSigSerialToken(stringIndex + 2, method.ContactMethodId.ToString()); + tokenArray[arrayIndex + 3] = new XSigSerialToken(stringIndex + 3, method.Device.ToString()); + + arrayIndex += offset; + stringIndex += maxStrings; + } + + while (arrayIndex < maxMethods) + { + tokenArray[arrayIndex + 1] = new XSigSerialToken(stringIndex + 1, String.Empty); + tokenArray[arrayIndex + 2] = new XSigSerialToken(stringIndex + 2, String.Empty); + tokenArray[arrayIndex + 3] = new XSigSerialToken(stringIndex + 3, String.Empty); + + arrayIndex += offset; + stringIndex += maxStrings; + } + + return GetXSigString(tokenArray); + } + + private string UpdateDirectoryXSig(CodecDirectory directory, bool isRoot) + { + var xSigMaxIndex = 1023; + var tokenArray = new XSigToken[directory.CurrentDirectoryResults.Count > xSigMaxIndex + ? xSigMaxIndex + : directory.CurrentDirectoryResults.Count]; + + Debug.LogMessage(LogEventLevel.Verbose, this, "IsRoot: {0}, Directory Count: {1}, TokenArray.Length: {2}", isRoot, directory.CurrentDirectoryResults.Count, tokenArray.Length); + + var contacts = directory.CurrentDirectoryResults.Count > xSigMaxIndex + ? directory.CurrentDirectoryResults.Take(xSigMaxIndex) + : directory.CurrentDirectoryResults; + + var contactsToDisplay = isRoot + ? contacts.Where(c => c.ParentFolderId == "root") + : contacts.Where(c => c.ParentFolderId != "root"); + + var counterIndex = 1; + foreach (var entry in contactsToDisplay) + { + var arrayIndex = counterIndex - 1; + var entryIndex = counterIndex; + + Debug.LogMessage(LogEventLevel.Verbose, this, "Entry{2:0000} Name: {0}, Folder ID: {1}, Type: {3}, ParentFolderId: {4}", + entry.Name, entry.FolderId, entryIndex, entry.GetType().FullName, entry.ParentFolderId); + + if (entry is DirectoryFolder) + { + tokenArray[arrayIndex] = new XSigSerialToken(entryIndex, String.Format("[+] {0}", entry.Name)); + + counterIndex++; + + continue; } - trilist.SetString(joinMap.DirectoryEntrySelectedName.JoinNumber, - selectedContact != null ? selectedContact.Name : string.Empty); + tokenArray[arrayIndex] = new XSigSerialToken(entryIndex, entry.Name); - // Allow auto dial of selected line. Always dials first contact method - if (!trilist.GetBool(joinMap.DirectoryDisableAutoDialSelectedLine.JoinNumber)) + counterIndex++; + } + + return GetXSigString(tokenArray); + } + + private void LinkVideoCodecCallControlsToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + trilist.SetSigFalseAction(joinMap.ManualDial.JoinNumber, + () => Dial(trilist.StringOutput[joinMap.CurrentDialString.JoinNumber].StringValue)); + + //End All calls + trilist.SetSigFalseAction(joinMap.EndAllCalls.JoinNumber, EndAllCalls); + + //End a specific call, specified by index. Maximum 8 calls supported + for (int i = 0; i < joinMap.EndCallStart.JoinSpan; i++) + { + var callIndex = i; + + trilist.SetSigFalseAction((uint)(joinMap.EndCallStart.JoinNumber + i), () => + { + + if (callIndex < 0 || callIndex >= ActiveCalls.Count) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Cannot end call. No call found at index: {0}", callIndex); + return; + } + + var call = ActiveCalls[callIndex]; + if (call != null) + { + EndCall(call); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "[End Call] Unable to find call at index '{0}'", i); + } + }); + } + + trilist.SetBool(joinMap.HookState.JoinNumber, IsInCall); + + CallStatusChange += (sender, args) => + { + trilist.SetBool(joinMap.HookState.JoinNumber, IsInCall); + + Debug.LogMessage(LogEventLevel.Debug, this, "Call Direction: {0}", args.CallItem.Direction); + Debug.LogMessage(LogEventLevel.Debug, this, "Call is incoming: {0}", args.CallItem.Direction == eCodecCallDirection.Incoming); + trilist.SetBool(joinMap.IncomingCall.JoinNumber, args.CallItem.Direction == eCodecCallDirection.Incoming && args.CallItem.Status == eCodecCallStatus.Ringing); + + if (args.CallItem.Direction == eCodecCallDirection.Incoming) { - if (_selectedDirectoryItem is IInvitableContact invitableEntry) - { - Dial(invitableEntry); - return; - } - - - trilist.SetString(joinMap.DirectoryEntrySelectedNumber.JoinNumber, - selectedContact != null ? selectedContact.ContactMethods[0].Number : string.Empty); - - if (!(_selectedDirectoryItem is DirectoryContact entryToDial)) return; - - Dial(entryToDial.ContactMethods[0].Number); + trilist.SetSigFalseAction(joinMap.IncomingAnswer.JoinNumber, () => AcceptCall(args.CallItem)); + trilist.SetSigFalseAction(joinMap.IncomingReject.JoinNumber, () => RejectCall(args.CallItem)); + trilist.SetString(joinMap.IncomingCallName.JoinNumber, args.CallItem.Name); + trilist.SetString(joinMap.IncomingCallNumber.JoinNumber, args.CallItem.Number); } else { - // If auto dial is disabled... + trilist.SetString(joinMap.IncomingCallName.JoinNumber, string.Empty); + trilist.SetString(joinMap.IncomingCallNumber.JoinNumber, string.Empty); + } - if (!(_selectedDirectoryItem is DirectoryContact entryToDial)) - { - // Clear out values and actions from last selected item - trilist.SetUshort(joinMap.SelectedContactMethodCount.JoinNumber, 0); - trilist.SetString(joinMap.DirectoryEntrySelectedName.JoinNumber, string.Empty); - trilist.ClearUShortSigAction(joinMap.SelectContactMethod.JoinNumber); - trilist.ClearBoolSigAction(joinMap.DirectoryDialSelectedLine.JoinNumber); - trilist.ClearBoolSigAction(joinMap.DirectoryDialSelectedContactMethod.JoinNumber); - return; - } - trilist.SetUshort(joinMap.SelectedContactMethodCount.JoinNumber, (ushort)entryToDial.ContactMethods.Count); + trilist.SetString(joinMap.CurrentCallData.JoinNumber, UpdateCallStatusXSig()); - // Update the action to dial the selected contact method - trilist.SetUShortSigAction(joinMap.SelectContactMethod.JoinNumber, (u) => - { - if (u < 1 || u > entryToDial.ContactMethods.Count) return; + trilist.SetUshort(joinMap.ConnectedCallCount.JoinNumber, (ushort)ActiveCalls.Count); + }; - trilist.SetSigFalseAction(joinMap.DirectoryDialSelectedContactMethod.JoinNumber, () => Dial(entryToDial.ContactMethods[u - 1].Number)); - }); + if (this is IJoinCalls joinCodec) + { + trilist.SetSigFalseAction(joinMap.JoinAllCalls.JoinNumber, () => joinCodec.JoinAllCalls()); - // Sets DirectoryDialSelectedLine join action to dial first contact method - trilist.SetSigFalseAction(joinMap.DirectoryDialSelectedLine.JoinNumber, () => Dial(entryToDial.ContactMethods[0].Number)); - - var clearBytes = XSigHelpers.ClearOutputs(); - - trilist.SetString(joinMap.ContactMethods.JoinNumber, - Encoding.GetEncoding(XSigEncoding).GetString(clearBytes, 0, clearBytes.Length)); - var contactMethodsXSig = UpdateContactMethodsXSig(entryToDial); - - trilist.SetString(joinMap.ContactMethods.JoinNumber, contactMethodsXSig); + for (int i = 0; i < joinMap.JoinCallStart.JoinSpan; i++) + { + trilist.SetSigFalseAction((uint)(joinMap.JoinCallStart.JoinNumber + i), () => + { + var call = ActiveCalls[i]; + if (call != null) + { + joinCodec.JoinCall(call); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "[Join Call] Unable to find call at index '{0}'", i); + } + }); } } - /// - /// Generates the XSig data representing the available contact methods for the selected DirectoryContact - /// - /// - /// - private string UpdateContactMethodsXSig(DirectoryContact contact) + if (this is IHasCallHold holdCodec) { - const int maxMethods = 10; - const int maxStrings = 3; - const int offset = maxStrings; - var stringIndex = 0; - var arrayIndex = 0; - // Create a new token array and set the size to the number of methods times the total number of signals - var tokenArray = new XSigToken[maxMethods * offset]; - - Debug.LogMessage(LogEventLevel.Verbose, this, "Creating XSIG token array with size {0}", maxMethods * offset); - - // TODO: Add code to generate XSig data - foreach (var method in contact.ContactMethods) + trilist.SetSigFalseAction(joinMap.HoldAllCalls.JoinNumber, () => { - if (arrayIndex >= maxMethods * offset) - break; + foreach (var call in ActiveCalls) + { + holdCodec.HoldCall(call); + } + }); - //serials - tokenArray[arrayIndex + 1] = new XSigSerialToken(stringIndex + 1, method.Number); - tokenArray[arrayIndex + 2] = new XSigSerialToken(stringIndex + 2, method.ContactMethodId.ToString()); - tokenArray[arrayIndex + 3] = new XSigSerialToken(stringIndex + 3, method.Device.ToString()); - - arrayIndex += offset; - stringIndex += maxStrings; - } - - while (arrayIndex < maxMethods) + for (int i = 0; i < joinMap.HoldCallsStart.JoinSpan; i++) { - tokenArray[arrayIndex + 1] = new XSigSerialToken(stringIndex + 1, String.Empty); - tokenArray[arrayIndex + 2] = new XSigSerialToken(stringIndex + 2, String.Empty); - tokenArray[arrayIndex + 3] = new XSigSerialToken(stringIndex + 3, String.Empty); + var index = i; - arrayIndex += offset; - stringIndex += maxStrings; + trilist.SetSigFalseAction((uint)(joinMap.HoldCallsStart.JoinNumber + index), () => + { + if (index < 0 || index >= ActiveCalls.Count) return; + + var call = ActiveCalls[index]; + if (call != null) + { + holdCodec.HoldCall(call); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "[Hold Call] Unable to find call at index '{0}'", i); + } + }); + + trilist.SetSigFalseAction((uint)(joinMap.ResumeCallsStart.JoinNumber + index), () => + { + if (index < 0 || index >= ActiveCalls.Count) return; + + var call = ActiveCalls[index]; + if (call != null) + { + holdCodec.ResumeCall(call); + } + else + { + Debug.LogMessage(LogEventLevel.Information, this, "[Resume Call] Unable to find call at index '{0}'", i); + } + }); } - - return GetXSigString(tokenArray); } - private string UpdateDirectoryXSig(CodecDirectory directory, bool isRoot) - { - var xSigMaxIndex = 1023; - var tokenArray = new XSigToken[directory.CurrentDirectoryResults.Count > xSigMaxIndex - ? xSigMaxIndex - : directory.CurrentDirectoryResults.Count]; - - Debug.LogMessage(LogEventLevel.Verbose, this, "IsRoot: {0}, Directory Count: {1}, TokenArray.Length: {2}", isRoot, directory.CurrentDirectoryResults.Count, tokenArray.Length); - - var contacts = directory.CurrentDirectoryResults.Count > xSigMaxIndex - ? directory.CurrentDirectoryResults.Take(xSigMaxIndex) - : directory.CurrentDirectoryResults; - - var contactsToDisplay = isRoot - ? contacts.Where(c => c.ParentFolderId == "root") - : contacts.Where(c => c.ParentFolderId != "root"); - - var counterIndex = 1; - foreach (var entry in contactsToDisplay) - { - var arrayIndex = counterIndex - 1; - var entryIndex = counterIndex; - - Debug.LogMessage(LogEventLevel.Verbose, this, "Entry{2:0000} Name: {0}, Folder ID: {1}, Type: {3}, ParentFolderId: {4}", - entry.Name, entry.FolderId, entryIndex, entry.GetType().FullName, entry.ParentFolderId); - - if (entry is DirectoryFolder) - { - tokenArray[arrayIndex] = new XSigSerialToken(entryIndex, String.Format("[+] {0}", entry.Name)); - - counterIndex++; - - continue; - } - - tokenArray[arrayIndex] = new XSigSerialToken(entryIndex, entry.Name); - - counterIndex++; - } - - return GetXSigString(tokenArray); - } - - private void LinkVideoCodecCallControlsToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - trilist.SetSigFalseAction(joinMap.ManualDial.JoinNumber, - () => Dial(trilist.StringOutput[joinMap.CurrentDialString.JoinNumber].StringValue)); - - //End All calls - trilist.SetSigFalseAction(joinMap.EndAllCalls.JoinNumber, EndAllCalls); - - //End a specific call, specified by index. Maximum 8 calls supported - for (int i = 0; i < joinMap.EndCallStart.JoinSpan; i++) - { - var callIndex = i; - - trilist.SetSigFalseAction((uint)(joinMap.EndCallStart.JoinNumber + i), () => - { - - if (callIndex < 0 || callIndex >= ActiveCalls.Count) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Cannot end call. No call found at index: {0}", callIndex); - return; - } - - var call = ActiveCalls[callIndex]; - if (call != null) - { - EndCall(call); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "[End Call] Unable to find call at index '{0}'", i); - } - }); - } - - trilist.SetBool(joinMap.HookState.JoinNumber, IsInCall); - - CallStatusChange += (sender, args) => - { - trilist.SetBool(joinMap.HookState.JoinNumber, IsInCall); - - Debug.LogMessage(LogEventLevel.Debug, this, "Call Direction: {0}", args.CallItem.Direction); - Debug.LogMessage(LogEventLevel.Debug, this, "Call is incoming: {0}", args.CallItem.Direction == eCodecCallDirection.Incoming); - trilist.SetBool(joinMap.IncomingCall.JoinNumber, args.CallItem.Direction == eCodecCallDirection.Incoming && args.CallItem.Status == eCodecCallStatus.Ringing); - - if (args.CallItem.Direction == eCodecCallDirection.Incoming) - { - trilist.SetSigFalseAction(joinMap.IncomingAnswer.JoinNumber, () => AcceptCall(args.CallItem)); - trilist.SetSigFalseAction(joinMap.IncomingReject.JoinNumber, () => RejectCall(args.CallItem)); - trilist.SetString(joinMap.IncomingCallName.JoinNumber, args.CallItem.Name); - trilist.SetString(joinMap.IncomingCallNumber.JoinNumber, args.CallItem.Number); - } - else - { - trilist.SetString(joinMap.IncomingCallName.JoinNumber, string.Empty); - trilist.SetString(joinMap.IncomingCallNumber.JoinNumber, string.Empty); - } - trilist.SetString(joinMap.CurrentCallData.JoinNumber, UpdateCallStatusXSig()); - - trilist.SetUshort(joinMap.ConnectedCallCount.JoinNumber, (ushort)ActiveCalls.Count); - }; - - if (this is IJoinCalls joinCodec) - { - trilist.SetSigFalseAction(joinMap.JoinAllCalls.JoinNumber, () => joinCodec.JoinAllCalls()); - - for (int i = 0; i < joinMap.JoinCallStart.JoinSpan; i++) - { - trilist.SetSigFalseAction((uint)(joinMap.JoinCallStart.JoinNumber + i), () => - { - var call = ActiveCalls[i]; - if (call != null) - { - joinCodec.JoinCall(call); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "[Join Call] Unable to find call at index '{0}'", i); - } - }); - } - } - - if (this is IHasCallHold holdCodec) - { - trilist.SetSigFalseAction(joinMap.HoldAllCalls.JoinNumber, () => - { - foreach (var call in ActiveCalls) - { - holdCodec.HoldCall(call); - } - }); - - for (int i = 0; i < joinMap.HoldCallsStart.JoinSpan; i++) - { - var index = i; - - trilist.SetSigFalseAction((uint)(joinMap.HoldCallsStart.JoinNumber + index), () => - { - if (index < 0 || index >= ActiveCalls.Count) return; - - var call = ActiveCalls[index]; - if (call != null) - { - holdCodec.HoldCall(call); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "[Hold Call] Unable to find call at index '{0}'", i); - } - }); - - trilist.SetSigFalseAction((uint)(joinMap.ResumeCallsStart.JoinNumber + index), () => - { - if (index < 0 || index >= ActiveCalls.Count) return; - - var call = ActiveCalls[index]; - if (call != null) - { - holdCodec.ResumeCall(call); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "[Resume Call] Unable to find call at index '{0}'", i); - } - }); - } - } - - - - trilist.OnlineStatusChange += (device, args) => + trilist.OnlineStatusChange += (device, args) => { if (!args.DeviceOnLine) return; @@ -1524,159 +1503,136 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec trilist.SetString(joinMap.CurrentCallData.JoinNumber, "\xFC"); trilist.SetString(joinMap.CurrentCallData.JoinNumber, UpdateCallStatusXSig()); }; + } + private string UpdateCallStatusXSig() + { + const int maxCalls = 8; + const int maxStrings = 6; + const int maxDigitals = 2; + const int offset = maxStrings + maxDigitals; + var stringIndex = 0; + var digitalIndex = maxStrings * maxCalls; + var arrayIndex = 0; + + var tokenArray = new XSigToken[maxCalls * offset]; //set array size for number of calls * pieces of info + + foreach (var call in ActiveCalls) + { + if (arrayIndex >= maxCalls * offset) + break; + //digitals + tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, call.IsActiveCall); + tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, call.IsOnHold); + + //serials + tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, call.Name ?? String.Empty); + tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, call.Number ?? String.Empty); + tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, call.Direction.ToString()); + tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, call.Type.ToString()); + tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, call.Status.ToString()); + if (call.Duration != null) + { + // May need to verify correct string format here + var dur = string.Format("{0:c}", call.Duration); + tokenArray[arrayIndex + 6] = new XSigSerialToken(stringIndex + 6, dur); + } + + arrayIndex += offset; + stringIndex += maxStrings; + digitalIndex += maxDigitals; + } + while (arrayIndex < maxCalls * offset) + { + //digitals + tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, false); + tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, false); + + + //serials + tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, String.Empty); + tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, String.Empty); + tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, String.Empty); + tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, String.Empty); + tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, String.Empty); + tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, String.Empty); + + arrayIndex += offset; + stringIndex += maxStrings; + digitalIndex += maxDigitals; } - private string UpdateCallStatusXSig() + return GetXSigString(tokenArray); + } + + private void LinkVideoCodecDtmfToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + trilist.SetSigFalseAction(joinMap.Dtmf0.JoinNumber, () => SendDtmfAction("0", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf1.JoinNumber, () => SendDtmfAction("1", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf2.JoinNumber, () => SendDtmfAction("2", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf3.JoinNumber, () => SendDtmfAction("3", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf4.JoinNumber, () => SendDtmfAction("4", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf5.JoinNumber, () => SendDtmfAction("5", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf6.JoinNumber, () => SendDtmfAction("6", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf7.JoinNumber, () => SendDtmfAction("7", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf8.JoinNumber, () => SendDtmfAction("8", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.Dtmf9.JoinNumber, () => SendDtmfAction("9", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.DtmfStar.JoinNumber, () => SendDtmfAction("*", trilist, joinMap)); + trilist.SetSigFalseAction(joinMap.DtmfPound.JoinNumber, () => SendDtmfAction("#", trilist, joinMap)); + } + + /// + /// Sends the specified string as a DTMF command. + /// Reads the value of the SendDtmfToSpecificCallInstance digital join and SelectCall analog join to determine + /// Whther to send to a specific call index or to the last connected call + /// + /// + /// + /// + private void SendDtmfAction(string s, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + if (!trilist.GetBool(joinMap.SendDtmfToSpecificCallIndex.JoinNumber)) { - const int maxCalls = 8; - const int maxStrings = 6; - const int maxDigitals = 2; - const int offset = maxStrings + maxDigitals; - var stringIndex = 0; - var digitalIndex = maxStrings * maxCalls; - var arrayIndex = 0; - - var tokenArray = new XSigToken[maxCalls * offset]; //set array size for number of calls * pieces of info - - foreach (var call in ActiveCalls) + SendDtmf(s); + } + else + { + var callIndex = trilist.GetUshort(joinMap.SelectCall.JoinNumber); + if (callIndex > 0 && callIndex <= 8) { - if (arrayIndex >= maxCalls * offset) - break; - //digitals - tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, call.IsActiveCall); - tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, call.IsOnHold); - - //serials - tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, call.Name ?? String.Empty); - tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, call.Number ?? String.Empty); - tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, call.Direction.ToString()); - tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, call.Type.ToString()); - tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, call.Status.ToString()); - if (call.Duration != null) + var call = ActiveCalls[callIndex - 1]; + if (call != null && call.IsActiveCall) { - // May need to verify correct string format here - var dur = string.Format("{0:c}", call.Duration); - tokenArray[arrayIndex + 6] = new XSigSerialToken(stringIndex + 6, dur); - } - - arrayIndex += offset; - stringIndex += maxStrings; - digitalIndex += maxDigitals; - } - while (arrayIndex < maxCalls * offset) - { - //digitals - tokenArray[digitalIndex] = new XSigDigitalToken(digitalIndex + 1, false); - tokenArray[digitalIndex + 1] = new XSigDigitalToken(digitalIndex + 2, false); - - - //serials - tokenArray[stringIndex] = new XSigSerialToken(stringIndex + 1, String.Empty); - tokenArray[stringIndex + 1] = new XSigSerialToken(stringIndex + 2, String.Empty); - tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, String.Empty); - tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, String.Empty); - tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, String.Empty); - tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, String.Empty); - - arrayIndex += offset; - stringIndex += maxStrings; - digitalIndex += maxDigitals; - } - - return GetXSigString(tokenArray); - } - - private void LinkVideoCodecDtmfToApi(BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - trilist.SetSigFalseAction(joinMap.Dtmf0.JoinNumber, () => SendDtmfAction("0", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf1.JoinNumber, () => SendDtmfAction("1", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf2.JoinNumber, () => SendDtmfAction("2", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf3.JoinNumber, () => SendDtmfAction("3", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf4.JoinNumber, () => SendDtmfAction("4", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf5.JoinNumber, () => SendDtmfAction("5", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf6.JoinNumber, () => SendDtmfAction("6", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf7.JoinNumber, () => SendDtmfAction("7", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf8.JoinNumber, () => SendDtmfAction("8", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.Dtmf9.JoinNumber, () => SendDtmfAction("9", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.DtmfStar.JoinNumber, () => SendDtmfAction("*", trilist, joinMap)); - trilist.SetSigFalseAction(joinMap.DtmfPound.JoinNumber, () => SendDtmfAction("#", trilist, joinMap)); - } - - /// - /// Sends the specified string as a DTMF command. - /// Reads the value of the SendDtmfToSpecificCallInstance digital join and SelectCall analog join to determine - /// Whther to send to a specific call index or to the last connected call - /// - /// - /// - /// - private void SendDtmfAction(string s, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - if (!trilist.GetBool(joinMap.SendDtmfToSpecificCallIndex.JoinNumber)) - { - SendDtmf(s); - } - else - { - var callIndex = trilist.GetUshort(joinMap.SelectCall.JoinNumber); - if (callIndex > 0 && callIndex <= 8) - { - var call = ActiveCalls[callIndex - 1]; - if (call != null && call.IsActiveCall) - { - SendDtmf(s, call); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Warning: No call found at index {0} or call is not active.", callIndex); - } + SendDtmf(s, call); } else { - Debug.LogMessage(LogEventLevel.Information, this, "Warning: Invalid call index specified. Please use a value of 1-8."); + Debug.LogMessage(LogEventLevel.Information, this, "Warning: No call found at index {0} or call is not active.", callIndex); } } - } - - private void LinkVideoCodecCameraLayoutsToApi(IHasCodecLayouts codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - trilist.SetSigFalseAction(joinMap.CameraLayout.JoinNumber, codec.LocalLayoutToggle); - - codec.LocalLayoutFeedback.LinkInputSig(trilist.StringInput[joinMap.CurrentLayoutStringFb.JoinNumber]); - } - - private void LinkVideoCodecCameraModeToApi(IHasCameraAutoMode codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - trilist.SetSigFalseAction(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeOn); - trilist.SetSigFalseAction(joinMap.CameraModeManual.JoinNumber, codec.CameraAutoModeOff); - - codec.CameraAutoModeIsOnFeedback.OutputChange += (o, a) => + else { - if (codec is IHasCameraOff offCodec) - { - if (offCodec.CameraIsOffFeedback.BoolValue) - { - trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false); - trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false); - trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true); - return; - } + Debug.LogMessage(LogEventLevel.Information, this, "Warning: Invalid call index specified. Please use a value of 1-8."); + } + } + } - trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, a.BoolValue); - trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !a.BoolValue); - trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); - return; - } + private void LinkVideoCodecCameraLayoutsToApi(IHasCodecLayouts codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + trilist.SetSigFalseAction(joinMap.CameraLayout.JoinNumber, codec.LocalLayoutToggle); - trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, a.BoolValue); - trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !a.BoolValue); - trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); - }; + codec.LocalLayoutFeedback.LinkInputSig(trilist.StringInput[joinMap.CurrentLayoutStringFb.JoinNumber]); + } + private void LinkVideoCodecCameraModeToApi(IHasCameraAutoMode codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + trilist.SetSigFalseAction(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeOn); + trilist.SetSigFalseAction(joinMap.CameraModeManual.JoinNumber, codec.CameraAutoModeOff); - if (codec is IHasCameraOff offModeCodec) + codec.CameraAutoModeIsOnFeedback.OutputChange += (o, a) => + { + if (codec is IHasCameraOff offCodec) { - if (offModeCodec.CameraIsOffFeedback.BoolValue) + if (offCodec.CameraIsOffFeedback.BoolValue) { trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false); trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false); @@ -1684,360 +1640,383 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec return; } - trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeIsOnFeedback.BoolValue); - trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !codec.CameraAutoModeIsOnFeedback.BoolValue); + trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, a.BoolValue); + trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !a.BoolValue); trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); return; } + trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, a.BoolValue); + trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !a.BoolValue); + trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); + }; + + + if (codec is IHasCameraOff offModeCodec) + { + if (offModeCodec.CameraIsOffFeedback.BoolValue) + { + trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false); + trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false); + trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true); + return; + } + trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeIsOnFeedback.BoolValue); trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !codec.CameraAutoModeIsOnFeedback.BoolValue); trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); + return; } - private void LinkVideoCodecSelfviewToApi(IHasCodecSelfView codec, BasicTriList trilist, - VideoCodecControllerJoinMap joinMap) - { - trilist.SetSigFalseAction(joinMap.CameraSelfView.JoinNumber, codec.SelfViewModeToggle); + trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeIsOnFeedback.BoolValue); + trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !codec.CameraAutoModeIsOnFeedback.BoolValue); + trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false); + } - codec.SelfviewIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.CameraSelfView.JoinNumber]); + private void LinkVideoCodecSelfviewToApi(IHasCodecSelfView codec, BasicTriList trilist, + VideoCodecControllerJoinMap joinMap) + { + trilist.SetSigFalseAction(joinMap.CameraSelfView.JoinNumber, codec.SelfViewModeToggle); + + codec.SelfviewIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.CameraSelfView.JoinNumber]); + } + + private void LinkVideoCodecCameraToApi(IHasCodecCameras codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + //Camera PTZ + trilist.SetBoolSigAction(joinMap.CameraTiltUp.JoinNumber, (b) => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; + + if (b) camera.TiltUp(); + else camera.TiltStop(); + }); + + trilist.SetBoolSigAction(joinMap.CameraTiltDown.JoinNumber, (b) => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; + + if (b) camera.TiltDown(); + else camera.TiltStop(); + }); + trilist.SetBoolSigAction(joinMap.CameraPanLeft.JoinNumber, (b) => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; + + if (b) camera.PanLeft(); + else camera.PanStop(); + }); + trilist.SetBoolSigAction(joinMap.CameraPanRight.JoinNumber, (b) => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; + + if (b) camera.PanRight(); + else camera.PanStop(); + }); + + trilist.SetBoolSigAction(joinMap.CameraZoomIn.JoinNumber, (b) => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; + + if (b) camera.ZoomIn(); + else camera.ZoomStop(); + }); + + trilist.SetBoolSigAction(joinMap.CameraZoomOut.JoinNumber, (b) => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; + + if (b) camera.ZoomOut(); + else camera.ZoomStop(); + }); + + + trilist.SetBoolSigAction(joinMap.CameraFocusNear.JoinNumber, (b) => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return; + + if (b) camera.FocusNear(); + else camera.FocusStop(); + }); + + trilist.SetBoolSigAction(joinMap.CameraFocusFar.JoinNumber, (b) => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return; + + if (b) camera.FocusFar(); + else camera.FocusStop(); + }); + + trilist.SetSigFalseAction(joinMap.CameraFocusAuto.JoinNumber, () => + { + if (codec.SelectedCamera == null) return; + + if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return; + + camera.TriggerAutoFocus(); + }); + + // Camera count + trilist.SetUshort(joinMap.CameraCount.JoinNumber, (ushort)codec.Cameras.Count); + + // Camera names + for (uint i = 0; i < joinMap.CameraNamesFb.JoinSpan; i++) + { + //Check the count first + if (i < codec.Cameras.Count && codec.Cameras[(int)i] != null) + { + trilist.SetString(joinMap.CameraNamesFb.JoinNumber + i, codec.Cameras[(int)i].Name); + } + else + { + trilist.SetString(joinMap.CameraNamesFb.JoinNumber + i, ""); + } } - private void LinkVideoCodecCameraToApi(IHasCodecCameras codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + //Camera Select + trilist.SetUShortSigAction(joinMap.CameraNumberSelect.JoinNumber, (i) => { - //Camera PTZ - trilist.SetBoolSigAction(joinMap.CameraTiltUp.JoinNumber, (b) => + if (i > 0 && i <= codec.Cameras.Count) { - if (codec.SelectedCamera == null) return; - - if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; - - if (b) camera.TiltUp(); - else camera.TiltStop(); - }); - - trilist.SetBoolSigAction(joinMap.CameraTiltDown.JoinNumber, (b) => + codec.SelectCamera(codec.Cameras[i - 1].Key); + } + else { - if (codec.SelectedCamera == null) return; + Debug.LogMessage(LogEventLevel.Information, this, "Unable to select. No camera found at index {0}", i); + } + }); - if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; + // Set initial selected camera feedback + if (codec.SelectedCamera != null) + { + trilist.SetUshort(joinMap.CameraNumberSelect.JoinNumber, (ushort)codec.Cameras.FindIndex((c) => c.Key == codec.SelectedCamera.Key)); + } - if (b) camera.TiltDown(); - else camera.TiltStop(); - }); - trilist.SetBoolSigAction(joinMap.CameraPanLeft.JoinNumber, (b) => + codec.CameraSelected += (sender, args) => + { + var i = (ushort)codec.Cameras.FindIndex((c) => c.Key == args.SelectedCamera.Key); + + trilist.SetUshort(joinMap.CameraNumberSelect.JoinNumber, (ushort)(i + 1)); + + if (codec is IHasCodecRoomPresets) { - if (codec.SelectedCamera == null) return; - - if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; - - if (b) camera.PanLeft(); - else camera.PanStop(); - }); - trilist.SetBoolSigAction(joinMap.CameraPanRight.JoinNumber, (b) => - { - if (codec.SelectedCamera == null) return; - - if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; - - if (b) camera.PanRight(); - else camera.PanStop(); - }); - - trilist.SetBoolSigAction(joinMap.CameraZoomIn.JoinNumber, (b) => - { - if (codec.SelectedCamera == null) return; - - if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; - - if (b) camera.ZoomIn(); - else camera.ZoomStop(); - }); - - trilist.SetBoolSigAction(joinMap.CameraZoomOut.JoinNumber, (b) => - { - if (codec.SelectedCamera == null) return; - - if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return; - - if (b) camera.ZoomOut(); - else camera.ZoomStop(); - }); - - - trilist.SetBoolSigAction(joinMap.CameraFocusNear.JoinNumber, (b) => - { - if (codec.SelectedCamera == null) return; - - if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return; - - if (b) camera.FocusNear(); - else camera.FocusStop(); - }); - - trilist.SetBoolSigAction(joinMap.CameraFocusFar.JoinNumber, (b) => - { - if (codec.SelectedCamera == null) return; - - if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return; - - if (b) camera.FocusFar(); - else camera.FocusStop(); - }); - - trilist.SetSigFalseAction(joinMap.CameraFocusAuto.JoinNumber, () => - { - if (codec.SelectedCamera == null) return; - - if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return; - - camera.TriggerAutoFocus(); - }); - - // Camera count - trilist.SetUshort(joinMap.CameraCount.JoinNumber, (ushort)codec.Cameras.Count); - - // Camera names - for (uint i = 0; i < joinMap.CameraNamesFb.JoinSpan; i++) - { - //Check the count first - if (i < codec.Cameras.Count && codec.Cameras[(int)i] != null) - { - trilist.SetString(joinMap.CameraNamesFb.JoinNumber + i, codec.Cameras[(int)i].Name); - } - else - { - trilist.SetString(joinMap.CameraNamesFb.JoinNumber + i, ""); - } + return; } - //Camera Select - trilist.SetUShortSigAction(joinMap.CameraNumberSelect.JoinNumber, (i) => + if (!(args.SelectedCamera is IHasCameraPresets)) { - if (i > 0 && i <= codec.Cameras.Count) - { - codec.SelectCamera(codec.Cameras[i - 1].Key); - } - else - { - Debug.LogMessage(LogEventLevel.Information, this, "Unable to select. No camera found at index {0}", i); - } - }); - - // Set initial selected camera feedback - if (codec.SelectedCamera != null) - { - trilist.SetUshort(joinMap.CameraNumberSelect.JoinNumber, (ushort)codec.Cameras.FindIndex((c) => c.Key == codec.SelectedCamera.Key)); + return; } - codec.CameraSelected += (sender, args) => - { - var i = (ushort)codec.Cameras.FindIndex((c) => c.Key == args.SelectedCamera.Key); + var cam = args.SelectedCamera as IHasCameraPresets; + SetCameraPresetNames(cam.Presets); - trilist.SetUshort(joinMap.CameraNumberSelect.JoinNumber, (ushort)(i + 1)); + (args.SelectedCamera as IHasCameraPresets).PresetsListHasChanged += (o, eventArgs) => SetCameraPresetNames(cam.Presets); - if (codec is IHasCodecRoomPresets) - { - return; - } - - if (!(args.SelectedCamera is IHasCameraPresets)) - { - return; - } - - var cam = args.SelectedCamera as IHasCameraPresets; - SetCameraPresetNames(cam.Presets); - - (args.SelectedCamera as IHasCameraPresets).PresetsListHasChanged += (o, eventArgs) => SetCameraPresetNames(cam.Presets); - - trilist.SetUShortSigAction(joinMap.CameraPresetSelect.JoinNumber, - (a) => - { - cam.PresetSelect(a); - trilist.SetUshort(joinMap.CameraPresetSelect.JoinNumber, a); - }); - - trilist.SetSigFalseAction(joinMap.CameraPresetSave.JoinNumber, - () => + trilist.SetUShortSigAction(joinMap.CameraPresetSelect.JoinNumber, + (a) => { - cam.PresetStore(trilist.UShortOutput[joinMap.CameraPresetSelect.JoinNumber].UShortValue, - String.Empty); - trilist.PulseBool(joinMap.CameraPresetSave.JoinNumber, 3000); + cam.PresetSelect(a); + trilist.SetUshort(joinMap.CameraPresetSelect.JoinNumber, a); }); - }; - - if (!(codec is IHasCodecRoomPresets)) return; - - var presetCodec = codec as IHasCodecRoomPresets; - - presetCodec.CodecRoomPresetsListHasChanged += - (sender, args) => SetCameraPresetNames(presetCodec.NearEndPresets); - - //Camera Presets - trilist.SetUShortSigAction(joinMap.CameraPresetSelect.JoinNumber, (i) => - { - presetCodec.CodecRoomPresetSelect(i); - }); - - - // Far End Presets - trilist.SetUShortSigAction(joinMap.FarEndPresetSelect.JoinNumber, (i) => - { - presetCodec.SelectFarEndPreset(i); - }); - trilist.SetSigFalseAction(joinMap.CameraPresetSave.JoinNumber, - () => - { - presetCodec.CodecRoomPresetStore( - trilist.UShortOutput[joinMap.CameraPresetSelect.JoinNumber].UShortValue, String.Empty); - trilist.PulseBool(joinMap.CameraPresetSave.JoinNumber, 3000); - }); - - trilist.OnlineStatusChange += (device, args) => - { - if (!args.DeviceOnLine) return; - - // TODO [ ] Issue #868 - trilist.SetString(joinMap.CameraPresetNames.JoinNumber, "\xFC"); - SetCameraPresetNames(presetCodec.NearEndPresets); - }; - } - - // Following fields only used for Bridging - private int _selectedRecentCallItemIndex; - private DirectoryItem _selectedDirectoryItem; - - private void LinkVideoCodecCallHistoryToApi(IHasCallHistory codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - codec.CallHistory.RecentCallsListHasChanged += (o, a) => - { - UpdateCallHistory(codec, trilist, joinMap); - }; - - // Selected item action and feedback - trilist.SetUShortSigAction(joinMap.SelectRecentCallItem.JoinNumber, (u) => - { - if (u == 0 || u > codec.CallHistory.RecentCalls.Count) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Recent Call History index out of range"); - return; - } - - _selectedRecentCallItemIndex = (int)(u - 1); - trilist.SetUshort(joinMap.SelectRecentCallItem.JoinNumber, u); - - var _selectedRecentCallItem = codec.CallHistory.RecentCalls[_selectedRecentCallItemIndex]; - - if (_selectedRecentCallItem != null) - { - trilist.SetString(joinMap.SelectedRecentCallName.JoinNumber, _selectedRecentCallItem.Name); - trilist.SetString(joinMap.SelectedRecentCallNumber.JoinNumber, _selectedRecentCallItem.Number); - trilist.SetSigFalseAction(joinMap.RemoveSelectedRecentCallItem.JoinNumber, () => codec.RemoveCallHistoryEntry(_selectedRecentCallItem)); - trilist.SetSigFalseAction(joinMap.DialSelectedRecentCallItem.JoinNumber, () => this.Dial(_selectedRecentCallItem.Number)); - } - else - { - trilist.SetString(joinMap.SelectedRecentCallName.JoinNumber, string.Empty); - trilist.SetString(joinMap.SelectedRecentCallNumber.JoinNumber, string.Empty); - trilist.ClearBoolSigAction(joinMap.RemoveSelectedRecentCallItem.JoinNumber); - trilist.ClearBoolSigAction(joinMap.DialSelectedRecentCallItem.JoinNumber); - } - }); - } - - - - private void UpdateCallHistory(IHasCallHistory codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) - { - // Clear out selected item - _selectedRecentCallItemIndex = 0; - - trilist.SetUshort(joinMap.SelectRecentCallItem.JoinNumber, 0); - trilist.SetString(joinMap.SelectedRecentCallName.JoinNumber, string.Empty); - trilist.SetString(joinMap.SelectedRecentCallNumber.JoinNumber, string.Empty); - trilist.ClearBoolSigAction(joinMap.RemoveSelectedRecentCallItem.JoinNumber); - // - - trilist.SetUshort(joinMap.RecentCallCount.JoinNumber, (ushort)codec.CallHistory.RecentCalls.Count); - - // Update the call history joins - var maxItems = joinMap.RecentCallNamesStart.JoinSpan; - - // Create history - uint index = 0; - for (uint i = 0; i < maxItems && i < codec.CallHistory.RecentCalls.Count; i++) - { - trilist.SetString(joinMap.RecentCallNamesStart.JoinNumber + i, codec.CallHistory.RecentCalls[(int)i].Name); - trilist.SetString(joinMap.RecentCallTimesStart.JoinNumber + i, codec.CallHistory.RecentCalls[(int)i].StartTime.ToShortTimeString()); - trilist.SetUshort(joinMap.RecentCallOccurrenceType.JoinNumber + i, (ushort)codec.CallHistory.RecentCalls[(int)i].OccurrenceType); - //i++; - index = i; - } - - //foreach(var item in codec.CallHistory.RecentCalls) - //{ - // trilist.SetString(joinMap.RecentCallNamesStart.JoinNumber + i, item.Name); - // trilist.SetString(joinMap.RecentCallTimesStart.JoinNumber + i, item.StartTime.ToShortTimeString()); - // trilist.SetUshort(joinMap.RecentCallOccurrenceType.JoinNumber + i, (ushort)item.OccurrenceType); - // i++; - //} - - // Clears existing items - for (uint j = index; j < maxItems; j++) - { - trilist.SetString(joinMap.RecentCallNamesStart.JoinNumber + j, string.Empty); - trilist.SetString(joinMap.RecentCallTimesStart.JoinNumber + j, string.Empty); - trilist.SetUshort(joinMap.RecentCallOccurrenceType.JoinNumber + j, 0); - } - } - - private string SetCameraPresetNames(IEnumerable presets) - { - return SetCameraPresetNames(presets.Select(p => p.Description).ToList()); - } - - private string SetCameraPresetNames(IEnumerable presets) - { - return SetCameraPresetNames(presets.Select(p => p.Description).ToList()); - } - - private string SetCameraPresetNames(ICollection presets) - { - var i = 1; //start index for xsig; - - var tokenArray = new XSigToken[presets.Count]; - - foreach (var preset in presets) - { - var cameraPreset = new XSigSerialToken(i, preset); - tokenArray[i - 1] = cameraPreset; - i++; - } - - return GetXSigString(tokenArray); - } - - private string GetXSigString(XSigToken[] tokenArray) - { - string returnString; - using (var s = new MemoryStream()) - { - using (var tw = new XSigTokenStreamWriter(s, true)) + () => { - tw.WriteXSigData(tokenArray); + cam.PresetStore(trilist.UShortOutput[joinMap.CameraPresetSelect.JoinNumber].UShortValue, + String.Empty); + trilist.PulseBool(joinMap.CameraPresetSave.JoinNumber, 3000); + }); + }; + + if (!(codec is IHasCodecRoomPresets)) return; + + var presetCodec = codec as IHasCodecRoomPresets; + + presetCodec.CodecRoomPresetsListHasChanged += + (sender, args) => SetCameraPresetNames(presetCodec.NearEndPresets); + + //Camera Presets + trilist.SetUShortSigAction(joinMap.CameraPresetSelect.JoinNumber, (i) => + { + presetCodec.CodecRoomPresetSelect(i); + }); + + + // Far End Presets + trilist.SetUShortSigAction(joinMap.FarEndPresetSelect.JoinNumber, (i) => + { + presetCodec.SelectFarEndPreset(i); + }); + + + trilist.SetSigFalseAction(joinMap.CameraPresetSave.JoinNumber, + () => + { + presetCodec.CodecRoomPresetStore( + trilist.UShortOutput[joinMap.CameraPresetSelect.JoinNumber].UShortValue, String.Empty); + trilist.PulseBool(joinMap.CameraPresetSave.JoinNumber, 3000); + }); + + trilist.OnlineStatusChange += (device, args) => + { + if (!args.DeviceOnLine) return; + + // TODO [ ] Issue #868 + trilist.SetString(joinMap.CameraPresetNames.JoinNumber, "\xFC"); + SetCameraPresetNames(presetCodec.NearEndPresets); + }; + } + + // Following fields only used for Bridging + private int _selectedRecentCallItemIndex; + private DirectoryItem _selectedDirectoryItem; + + private void LinkVideoCodecCallHistoryToApi(IHasCallHistory codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + codec.CallHistory.RecentCallsListHasChanged += (o, a) => + { + UpdateCallHistory(codec, trilist, joinMap); + }; + + // Selected item action and feedback + trilist.SetUShortSigAction(joinMap.SelectRecentCallItem.JoinNumber, (u) => + { + if (u == 0 || u > codec.CallHistory.RecentCalls.Count) + { + Debug.LogMessage(LogEventLevel.Verbose, this, "Recent Call History index out of range"); + return; } - var xSig = s.ToArray(); + _selectedRecentCallItemIndex = (int)(u - 1); + trilist.SetUshort(joinMap.SelectRecentCallItem.JoinNumber, u); - returnString = Encoding.GetEncoding(XSigEncoding).GetString(xSig, 0, xSig.Length); - } + var _selectedRecentCallItem = codec.CallHistory.RecentCalls[_selectedRecentCallItemIndex]; - return returnString; + if (_selectedRecentCallItem != null) + { + trilist.SetString(joinMap.SelectedRecentCallName.JoinNumber, _selectedRecentCallItem.Name); + trilist.SetString(joinMap.SelectedRecentCallNumber.JoinNumber, _selectedRecentCallItem.Number); + trilist.SetSigFalseAction(joinMap.RemoveSelectedRecentCallItem.JoinNumber, () => codec.RemoveCallHistoryEntry(_selectedRecentCallItem)); + trilist.SetSigFalseAction(joinMap.DialSelectedRecentCallItem.JoinNumber, () => this.Dial(_selectedRecentCallItem.Number)); + } + else + { + trilist.SetString(joinMap.SelectedRecentCallName.JoinNumber, string.Empty); + trilist.SetString(joinMap.SelectedRecentCallNumber.JoinNumber, string.Empty); + trilist.ClearBoolSigAction(joinMap.RemoveSelectedRecentCallItem.JoinNumber); + trilist.ClearBoolSigAction(joinMap.DialSelectedRecentCallItem.JoinNumber); + } + }); + } + + + + private void UpdateCallHistory(IHasCallHistory codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap) + { + // Clear out selected item + _selectedRecentCallItemIndex = 0; + + trilist.SetUshort(joinMap.SelectRecentCallItem.JoinNumber, 0); + trilist.SetString(joinMap.SelectedRecentCallName.JoinNumber, string.Empty); + trilist.SetString(joinMap.SelectedRecentCallNumber.JoinNumber, string.Empty); + trilist.ClearBoolSigAction(joinMap.RemoveSelectedRecentCallItem.JoinNumber); + // + + trilist.SetUshort(joinMap.RecentCallCount.JoinNumber, (ushort)codec.CallHistory.RecentCalls.Count); + + // Update the call history joins + var maxItems = joinMap.RecentCallNamesStart.JoinSpan; + + // Create history + uint index = 0; + for (uint i = 0; i < maxItems && i < codec.CallHistory.RecentCalls.Count; i++) + { + trilist.SetString(joinMap.RecentCallNamesStart.JoinNumber + i, codec.CallHistory.RecentCalls[(int)i].Name); + trilist.SetString(joinMap.RecentCallTimesStart.JoinNumber + i, codec.CallHistory.RecentCalls[(int)i].StartTime.ToShortTimeString()); + trilist.SetUshort(joinMap.RecentCallOccurrenceType.JoinNumber + i, (ushort)codec.CallHistory.RecentCalls[(int)i].OccurrenceType); + //i++; + index = i; } - #endregion + //foreach(var item in codec.CallHistory.RecentCalls) + //{ + // trilist.SetString(joinMap.RecentCallNamesStart.JoinNumber + i, item.Name); + // trilist.SetString(joinMap.RecentCallTimesStart.JoinNumber + i, item.StartTime.ToShortTimeString()); + // trilist.SetUshort(joinMap.RecentCallOccurrenceType.JoinNumber + i, (ushort)item.OccurrenceType); + // i++; + //} + + // Clears existing items + for (uint j = index; j < maxItems; j++) + { + trilist.SetString(joinMap.RecentCallNamesStart.JoinNumber + j, string.Empty); + trilist.SetString(joinMap.RecentCallTimesStart.JoinNumber + j, string.Empty); + trilist.SetUshort(joinMap.RecentCallOccurrenceType.JoinNumber + j, 0); + } } -} \ No newline at end of file + + private string SetCameraPresetNames(IEnumerable presets) + { + return SetCameraPresetNames(presets.Select(p => p.Description).ToList()); + } + + private string SetCameraPresetNames(IEnumerable presets) + { + return SetCameraPresetNames(presets.Select(p => p.Description).ToList()); + } + + private string SetCameraPresetNames(ICollection presets) + { + var i = 1; //start index for xsig; + + var tokenArray = new XSigToken[presets.Count]; + + foreach (var preset in presets) + { + var cameraPreset = new XSigSerialToken(i, preset); + tokenArray[i - 1] = cameraPreset; + i++; + } + + return GetXSigString(tokenArray); + } + + private string GetXSigString(XSigToken[] tokenArray) + { + string returnString; + using (var s = new MemoryStream()) + { + using (var tw = new XSigTokenStreamWriter(s, true)) + { + tw.WriteXSigData(tokenArray); + } + + var xSig = s.ToArray(); + + returnString = Encoding.GetEncoding(XSigEncoding).GetString(xSig, 0, xSig.Length); + } + + return returnString; + } + + #endregion +} + + diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs b/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs index ab60d3f7..67aea972 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs @@ -8,16 +8,16 @@ namespace PepperDash.Essentials.AppServer /// public class SourceSelectMessageContent { - - [JsonProperty("sourceListItemKey")] /// /// Gets or sets the SourceListItemKey /// + [JsonProperty("sourceListItemKey")] public string SourceListItemKey { get; set; } - [JsonProperty("sourceListKey")] /// /// Gets or sets the SourceListKey /// + [JsonProperty("sourceListKey")] + public string SourceListKey { get; set; } } @@ -27,20 +27,20 @@ namespace PepperDash.Essentials.AppServer public class DirectRoute { - [JsonProperty("sourceKey")] /// /// Gets or sets the SourceKey /// + [JsonProperty("sourceKey")] public string SourceKey { get; set; } - [JsonProperty("destinationKey")] /// /// Gets or sets the DestinationKey /// + [JsonProperty("destinationKey")] public string DestinationKey { get; set; } - [JsonProperty("signalType")] /// /// Gets or sets the SignalType /// + [JsonProperty("signalType")] public eRoutingSignalType SignalType { get; set; } } @@ -52,4 +52,4 @@ namespace PepperDash.Essentials.AppServer /// Delegate for PressAndHoldAction /// public delegate void PressAndHoldAction(bool b); -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs index 349b8d58..5ce9261e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs @@ -15,11 +15,18 @@ namespace PepperDash.Essentials.Room.MobileControl { private readonly DisplayBase display; + /// + /// Create an instance of the class. + /// + /// + /// + /// public DisplayBaseMessenger(string key, string messagePath, DisplayBase device) : base(key, messagePath, device) { display = device; } + /// protected override void RegisterActions() { base.RegisterActions(); @@ -60,4 +67,4 @@ namespace PepperDash.Essentials.Room.MobileControl }); } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs index 505fd214..8d185804 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs @@ -11,11 +11,18 @@ namespace PepperDash.Essentials.Room.MobileControl { private readonly IChannel channelDevice; + /// + /// Create an instance of the class. + /// + /// + /// + /// public IChannelMessenger(string key, string messagePath, IChannel device) : base(key, messagePath, device as IKeyName) { channelDevice = device; } + /// protected override void RegisterActions() { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs index cef6111f..2a50c4a8 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs @@ -10,11 +10,19 @@ namespace PepperDash.Essentials.Room.MobileControl public class IColorMessenger : MessengerBase { private readonly IColor colorDevice; + + /// + /// Create an instance of the class. + /// + /// + /// + /// public IColorMessenger(string key, string messagePath, IColor device) : base(key, messagePath, device as IKeyName) { colorDevice = device as IColor; } + /// protected override void RegisterActions() { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs index 92c21a0b..d7207e09 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs @@ -10,12 +10,19 @@ namespace PepperDash.Essentials.Room.MobileControl public class IDPadMessenger : MessengerBase { private readonly IDPad dpadDevice; + + /// + /// Create an instance of the class. + /// + /// + /// + /// public IDPadMessenger(string key, string messagePath, IDPad device) : base(key, messagePath, device as IKeyName) { dpadDevice = device; } - + /// protected override void RegisterActions() { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs index f0ae426c..08b1d100 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs @@ -1,3 +1,4 @@ + using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; @@ -10,11 +11,20 @@ namespace PepperDash.Essentials.Room.MobileControl public class IDvrMessenger : MessengerBase { private readonly IDvr dvrDevice; + + /// + /// Create an instance of the class. + /// + /// + /// + /// public IDvrMessenger(string key, string messagePath, IDvr device) : base(key, messagePath, device as IKeyName) { dvrDevice = device; } + + /// protected override void RegisterActions() { base.RegisterActions(); @@ -24,4 +34,4 @@ namespace PepperDash.Essentials.Room.MobileControl } } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs index 864c20bf..247b533d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs @@ -10,11 +10,20 @@ namespace PepperDash.Essentials.Room.MobileControl public class IHasPowerMessenger : MessengerBase { private readonly IHasPowerControl powerDevice; + + /// + /// Create an instance of the class. + /// + /// + /// + /// public IHasPowerMessenger(string key, string messagePath, IHasPowerControl device) : base(key, messagePath, device as IKeyName) { powerDevice = device; } + + /// protected override void RegisterActions() { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs index d07331d2..746caf18 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs @@ -10,11 +10,20 @@ namespace PepperDash.Essentials.Room.MobileControl public class INumericKeypadMessenger : MessengerBase { private readonly INumericKeypad keypadDevice; + + /// + /// Create an instance of the class. + /// + /// + /// + /// public INumericKeypadMessenger(string key, string messagePath, INumericKeypad device) : base(key, messagePath, device as IKeyName) { keypadDevice = device; } + + /// protected override void RegisterActions() { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs index f87480bd..5e5a286d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs @@ -1,3 +1,4 @@ + using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; @@ -10,11 +11,20 @@ namespace PepperDash.Essentials.Room.MobileControl public class ISetTopBoxControlsMessenger : MessengerBase { private readonly ISetTopBoxControls stbDevice; + + /// + /// Create an instance of the class. + /// + /// + /// + /// public ISetTopBoxControlsMessenger(string key, string messagePath, ISetTopBoxControls device) : base(key, messagePath, device as IKeyName) { stbDevice = device; } + + /// protected override void RegisterActions() { base.RegisterActions(); @@ -35,8 +45,11 @@ namespace PepperDash.Essentials.Room.MobileControl } } + /// + /// Represents the state of a ISetTopBoxControls device to be sent to mobile clients + /// public class SetTopBoxControlsState : DeviceStateMessageBase { } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs index 0943fc0b..9eba0848 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs @@ -1,3 +1,4 @@ + using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; @@ -10,11 +11,19 @@ namespace PepperDash.Essentials.Room.MobileControl public class ITransportMessenger : MessengerBase { private readonly ITransport transportDevice; + + /// + /// Create an instance of the class. + /// + /// + /// + /// public ITransportMessenger(string key, string messagePath, ITransport device) : base(key, messagePath, device as IKeyName) { transportDevice = device; } + /// protected override void RegisterActions() { base.RegisterActions(); @@ -30,4 +39,4 @@ namespace PepperDash.Essentials.Room.MobileControl } } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs index 876bcafe..15e645a6 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs @@ -94,7 +94,10 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class DeviceInfoStateMessage : DeviceStateMessageBase { + /// + /// Gets or sets the device information associated with this message. This property contains detailed information + /// [JsonProperty("deviceInfo")] public DeviceInfo DeviceInfo { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs index 81df6bac..ff38a784 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs @@ -169,10 +169,10 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class VolumeStateMessage : DeviceStateMessageBase { - [JsonProperty("volume", NullValueHandling = NullValueHandling.Ignore)] /// /// Gets or sets the Volume /// + [JsonProperty("volume", NullValueHandling = NullValueHandling.Ignore)] public Volume Volume { get; set; } } @@ -181,27 +181,42 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class Volume { + /// + /// Gets or sets the Level + /// [JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)] public int? Level { get; set; } + + /// + /// Gets or sets the HasMute + /// [JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)] public bool? HasMute { get; set; } + + /// + /// Gets or sets the Muted + /// [JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)] public bool? Muted { get; set; } - [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] + /// /// Gets or sets the Label /// + [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] public string Label { get; set; } - [JsonProperty("rawValue", NullValueHandling = NullValueHandling.Ignore)] /// /// Gets or sets the RawValue /// + [JsonProperty("rawValue", NullValueHandling = NullValueHandling.Ignore)] public string RawValue { get; set; } + /// + /// Gets or sets the Units + /// [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("units", NullValueHandling = NullValueHandling.Ignore)] public eVolumeLevelUnits? Units { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs index 83def192..e6a334d3 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs @@ -32,4 +32,4 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(state, id); } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs index 79622a41..e2bd1be5 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs @@ -87,4 +87,4 @@ namespace PepperDash.Essentials.AppServer.Messengers } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs index 92674574..fff90dab 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs @@ -12,12 +12,20 @@ namespace PepperDash.Essentials.AppServer.Messengers { private readonly IDspPresets device; + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public IDspPresetsMessenger(string key, string messagePath, IDspPresets device) : base(key, messagePath, device as IKeyName) { this.device = device; } + + /// protected override void RegisterActions() { base.RegisterActions(); @@ -54,7 +62,11 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class IHasDspPresetsStateMessage : DeviceStateMessageBase { + /// + /// Gets or sets the presets. + /// The key is the preset key, and the value is the preset name + /// [JsonProperty("presets")] public Dictionary Presets { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs index 966d8d77..f705317e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs @@ -214,4 +214,4 @@ namespace PepperDash.Essentials.AppServer.Messengers } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs index 6c0f1a0a..2a8dd5bc 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs @@ -73,4 +73,4 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("currentSource")] public SourceListItem CurrentSource { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasInputsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasInputsMessenger.cs index 57ab2617..8d8949ed 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasInputsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasInputsMessenger.cs @@ -26,6 +26,8 @@ namespace PepperDash.Essentials.AppServer.Messengers itemDevice = device; } + + /// protected override void RegisterActions() { base.RegisterActions(); @@ -90,10 +92,10 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class IHasInputsStateMessage : DeviceStateMessageBase { - [JsonProperty("inputs")] /// /// Gets or sets the Inputs /// + [JsonProperty("inputs")] public Inputs Inputs { get; set; } } @@ -102,14 +104,18 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class Inputs { + /// + /// Gets or sets the Items + /// The key is the input key, and the value is the input name + /// [JsonProperty("items")] public Dictionary Items { get; set; } - [JsonProperty("currentItem")] /// /// Gets or sets the CurrentItem /// + [JsonProperty("currentItem")] public TKey CurrentItem { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs index 525a6d6d..b16611cf 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs @@ -70,4 +70,4 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("powerState", NullValueHandling = NullValueHandling.Ignore)] public bool? PowerState { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs index 76598cee..affd2d95 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs @@ -5,114 +5,76 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Devices.Common.Codec; -namespace PepperDash.Essentials.AppServer.Messengers +namespace PepperDash.Essentials.AppServer.Messengers; + +public class IHasScheduleAwarenessMessenger : MessengerBase { - /// - /// Represents a IHasScheduleAwarenessMessenger - /// - public class IHasScheduleAwarenessMessenger : MessengerBase + public IHasScheduleAwareness ScheduleSource { get; private set; } + + public IHasScheduleAwarenessMessenger(string key, IHasScheduleAwareness scheduleSource, string messagePath) + : base(key, messagePath, scheduleSource as IKeyName) { - /// - /// Gets or sets the ScheduleSource - /// - public IHasScheduleAwareness ScheduleSource { get; private set; } + ScheduleSource = scheduleSource ?? throw new ArgumentNullException("scheduleSource"); + ScheduleSource.CodecSchedule.MeetingsListHasChanged += new EventHandler(CodecSchedule_MeetingsListHasChanged); + ScheduleSource.CodecSchedule.MeetingEventChange += new EventHandler(CodecSchedule_MeetingEventChange); + } - public IHasScheduleAwarenessMessenger(string key, IHasScheduleAwareness scheduleSource, string messagePath) - : base(key, messagePath, scheduleSource as IKeyName) + protected override void RegisterActions() + { + AddAction("/schedule/fullStatus", (id, content) => SendFullScheduleObject()); + } + + private void CodecSchedule_MeetingEventChange(object sender, MeetingEventArgs e) + { + PostStatusMessage(JToken.FromObject(new MeetingChangeMessage { - ScheduleSource = scheduleSource ?? throw new ArgumentNullException("scheduleSource"); - ScheduleSource.CodecSchedule.MeetingsListHasChanged += new EventHandler(CodecSchedule_MeetingsListHasChanged); - ScheduleSource.CodecSchedule.MeetingEventChange += new EventHandler(CodecSchedule_MeetingEventChange); - } - - protected override void RegisterActions() - { - AddAction("/schedule/fullStatus", (id, content) => SendFullScheduleObject(id)); - - AddAction("/schedule/status", (id, content) => SendFullScheduleObject(id)); - } - - private void CodecSchedule_MeetingEventChange(object sender, MeetingEventArgs e) - { - PostStatusMessage(JToken.FromObject(new MeetingChangeMessage + MeetingChange = new MeetingChange { - MeetingChange = new MeetingChange - { - ChangeType = e.ChangeType.ToString(), - Meeting = e.Meeting - } - }) - ); - } + ChangeType = e.ChangeType.ToString(), + Meeting = e.Meeting + } + }) + ); + } - private void CodecSchedule_MeetingsListHasChanged(object sender, EventArgs e) + private void CodecSchedule_MeetingsListHasChanged(object sender, EventArgs e) + { + SendFullScheduleObject(); + } + + /// + /// Helper method to send the full schedule data + /// + private void SendFullScheduleObject() + { + PostStatusMessage(new FullScheduleMessage { - SendFullScheduleObject(); - } - - /// - /// Helper method to send the full schedule data - /// - private void SendFullScheduleObject(string id = null) - { - PostStatusMessage(new FullScheduleMessage - { - Meetings = ScheduleSource.CodecSchedule.Meetings, - MeetingWarningMinutes = ScheduleSource.CodecSchedule.MeetingWarningMinutes - }, id); - } + Meetings = ScheduleSource.CodecSchedule.Meetings, + MeetingWarningMinutes = ScheduleSource.CodecSchedule.MeetingWarningMinutes + }); } +} - /// - /// Represents a FullScheduleMessage - /// - public class FullScheduleMessage : DeviceStateMessageBase - { +public class FullScheduleMessage : DeviceStateMessageBase +{ + [JsonProperty("meetings", NullValueHandling = NullValueHandling.Ignore)] + public List Meetings { get; set; } - /// - /// Gets or sets the Meetings - /// - [JsonProperty("meetings", NullValueHandling = NullValueHandling.Ignore)] - public List Meetings { get; set; } + [JsonProperty("meetingWarningMinutes", NullValueHandling = NullValueHandling.Ignore)] + public int MeetingWarningMinutes { get; set; } +} +public class MeetingChangeMessage +{ + [JsonProperty("meetingChange", NullValueHandling = NullValueHandling.Ignore)] + public MeetingChange MeetingChange { get; set; } +} - /// - /// Gets or sets the MeetingWarningMinutes - /// - [JsonProperty("meetingWarningMinutes", NullValueHandling = NullValueHandling.Ignore)] - public int MeetingWarningMinutes { get; set; } - } +public class MeetingChange +{ + [JsonProperty("changeType", NullValueHandling = NullValueHandling.Ignore)] + public string ChangeType { get; set; } - /// - /// Represents a MeetingChangeMessage - /// - public class MeetingChangeMessage - { - - /// - /// Gets or sets the MeetingChange - /// - [JsonProperty("meetingChange", NullValueHandling = NullValueHandling.Ignore)] - public MeetingChange MeetingChange { get; set; } - } - - /// - /// Represents a MeetingChange - /// - public class MeetingChange - { - - /// - /// Gets or sets the ChangeType - /// - [JsonProperty("changeType", NullValueHandling = NullValueHandling.Ignore)] - public string ChangeType { get; set; } - - - /// - /// Gets or sets the Meeting - /// - [JsonProperty("meeting", NullValueHandling = NullValueHandling.Ignore)] - public Meeting Meeting { get; set; } - } + [JsonProperty("meeting", NullValueHandling = NullValueHandling.Ignore)] + public Meeting Meeting { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs index 6bd04a74..1fad0641 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs @@ -12,12 +12,19 @@ namespace PepperDash.Essentials.AppServer.Messengers { private readonly IHumiditySensor device; + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public IHumiditySensorMessenger(string key, IHumiditySensor device, string messagePath) : base(key, messagePath, device as IKeyName) { this.device = device; } + /// protected override void RegisterActions() { base.RegisterActions(); @@ -52,4 +59,4 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("humidity")] public string Humidity { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs index 03bfc80f..6ddb671d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs @@ -14,11 +14,18 @@ namespace PepperDash.Essentials.AppServer.Messengers { private ILevelControls levelControlsDevice; + /// + /// Create an instance of the class. + /// + /// + /// + /// public ILevelControlsMessenger(string key, string messagePath, ILevelControls device) : base(key, messagePath, device as IKeyName) { levelControlsDevice = device; } + /// protected override void RegisterActions() { base.RegisterActions(); @@ -87,6 +94,9 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class LevelControlStateMessage : DeviceStateMessageBase { + /// + /// Gets or sets the Levels + /// [JsonProperty("levelControls")] public Dictionary Levels { get; set; } } @@ -96,13 +106,16 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class LevelControlRequestMessage { - [JsonProperty("key")] /// /// Gets or sets the Key /// + [JsonProperty("key")] public string Key { get; set; } + /// + /// Represents a Volume + /// [JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)] public ushort? Level { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs index ec718919..3ea457d2 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs @@ -16,11 +16,20 @@ namespace PepperDash.Essentials.AppServer.Messengers public class IMatrixRoutingMessenger : MessengerBase { private readonly IMatrixRouting matrixDevice; - public IMatrixRoutingMessenger(string key, string messagePath, IMatrixRouting device) : base(key, messagePath, device as IKeyName) + + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public IMatrixRoutingMessenger(string key, string messagePath, IMatrixRouting device) : base(key, messagePath, device as IKeyName) { matrixDevice = device; } + /// protected override void RegisterActions() { base.RegisterActions(); @@ -99,9 +108,15 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class MatrixStateMessage : DeviceStateMessageBase { + /// + /// Gets or sets the Outputs + /// [JsonProperty("outputs")] public Dictionary Outputs; + /// + /// Gets or sets the Inputs + /// [JsonProperty("inputs")] public Dictionary Inputs; } @@ -109,33 +124,60 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Represents a RoutingInput /// - public class RoutingInput + public class RoutingInput : IKeyName { private IRoutingInputSlot _input; + /// + /// Gets the TxDeviceKey of the input slot + /// [JsonProperty("txDeviceKey", NullValueHandling = NullValueHandling.Ignore)] public string TxDeviceKey => _input?.TxDeviceKey; + /// + /// Gets the SlotNumber of the input slot + /// + /// [JsonProperty("slotNumber", NullValueHandling = NullValueHandling.Ignore)] public int? SlotNumber => _input?.SlotNumber; + /// + /// Gets the SupportedSignalTypes of the input slot + /// [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] [JsonProperty("supportedSignalTypes", NullValueHandling = NullValueHandling.Ignore)] public eRoutingSignalType? SupportedSignalTypes => _input?.SupportedSignalTypes; + /// + /// Gets the Name of the input slot + /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name => _input?.Name; + /// + /// Gets the IsOnline of the input slot + /// + /// [JsonProperty("isOnline", NullValueHandling = NullValueHandling.Ignore)] public bool? IsOnline => _input?.IsOnline.BoolValue; - [JsonProperty("videoSyncDetected", NullValueHandling = NullValueHandling.Ignore)] + /// + /// Gets the VideoSyncDetected of the input slot + /// + [JsonProperty("videoSyncDetected", NullValueHandling = NullValueHandling.Ignore)] public bool? VideoSyncDetected => _input?.VideoSyncDetected; + /// + /// Gets the Key of the input slot + /// [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] public string Key => _input?.Key; + /// + /// Initializes a new instance of the class. + /// + /// public RoutingInput(IRoutingInputSlot input) { _input = input; @@ -145,32 +187,55 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Represents a RoutingOutput /// - public class RoutingOutput + public class RoutingOutput : IKeyName { private IRoutingOutputSlot _output; - + /// + /// Initializes a new instance of the class. + /// + /// public RoutingOutput(IRoutingOutputSlot output) { _output = output; } + /// + /// Gets the RxDeviceKey of the output slot + /// [JsonProperty("rxDeviceKey")] public string RxDeviceKey => _output.RxDeviceKey; + /// + /// Gets the CurrentRoutes of the output slot + /// [JsonProperty("currentRoutes")] public Dictionary CurrentRoutes => _output.CurrentRoutes.ToDictionary(kvp => kvp.Key.ToString(), kvp => new RoutingInput(kvp.Value)); + /// + /// Gets the SlotNumber of the output slot + /// [JsonProperty("slotNumber")] public int SlotNumber => _output.SlotNumber; + /// + /// Gets the SupportedSignalTypes of the output slot + /// [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] [JsonProperty("supportedSignalTypes")] public eRoutingSignalType SupportedSignalTypes => _output.SupportedSignalTypes; + + /// + /// Gets the Name of the output slot + /// [JsonProperty("name")] public string Name => _output.Name; + + /// + /// Gets the Key of the output slot + /// [JsonProperty("key")] public string Key => _output.Key; } @@ -180,22 +245,23 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class MatrixRouteRequest { - [JsonProperty("outputKey")] /// /// Gets or sets the OutputKey /// + [JsonProperty("outputKey")] public string OutputKey { get; set; } - [JsonProperty("inputKey")] /// /// Gets or sets the InputKey /// + [JsonProperty("inputKey")] public string InputKey { get; set; } - [JsonProperty("routeType")] /// /// Gets or sets the RouteType /// + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + [JsonProperty("routeType")] public eRoutingSignalType RouteType { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs index f63b4834..a02f0f99 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs @@ -101,4 +101,4 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public eScreenLiftControlType Type { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs index 8e2f8f3e..df1315a3 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs @@ -158,4 +158,4 @@ namespace PepperDash.Essentials.AppServer.Messengers public TKey CurrentItem { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs index 516e77c6..f3b21d7b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs @@ -12,12 +12,19 @@ namespace PepperDash.Essentials.AppServer.Messengers { private readonly IShutdownPromptTimer _room; + /// + /// Create an instance of the class. + /// + /// + /// + /// public IShutdownPromptTimerMessenger(string key, string messagePath, IShutdownPromptTimer room) : base(key, messagePath, room as IKeyName) { _room = room; } + /// protected override void RegisterActions() { AddAction("/status", (id, content) => SendFullStatus(id)); @@ -104,4 +111,4 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("shutdownPromptSeconds")] public int ShutdownPromptSeconds { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs index 98223188..ec4acfd8 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs @@ -13,12 +13,20 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly ISwitchedOutput device; + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public ISwitchedOutputMessenger(string key, ISwitchedOutput device, string messagePath) : base(key, messagePath, device as IKeyName) { this.device = device; } + + /// protected override void RegisterActions() { base.RegisterActions(); @@ -66,4 +74,4 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("isOn")] public bool IsOn { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs index 283ef0c8..c511c68c 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs @@ -11,12 +11,19 @@ namespace PepperDash.Essentials.AppServer.Messengers { private readonly ITechPassword _room; + /// + /// Constructor for ITechPasswordMessenger + /// + /// + /// + /// public ITechPasswordMessenger(string key, string messagePath, ITechPassword room) : base(key, messagePath, room as IKeyName) { _room = room; } + /// protected override void RegisterActions() { @@ -71,6 +78,9 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class ITechPasswordStateMessage : DeviceStateMessageBase { + /// + /// Gets or sets the TechPasswordLength + /// [JsonProperty("techPasswordLength", NullValueHandling = NullValueHandling.Ignore)] public int? TechPasswordLength { get; set; } } @@ -80,23 +90,26 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class ITechPasswordEventMessage : DeviceEventMessageBase { + /// + /// Gets or sets the IsValid + /// [JsonProperty("isValid", NullValueHandling = NullValueHandling.Ignore)] public bool? IsValid { get; set; } } internal class SetTechPasswordContent { - [JsonProperty("oldPassword")] /// /// Gets or sets the OldPassword /// + [JsonProperty("oldPassword")] public string OldPassword { get; set; } - [JsonProperty("newPassword")] /// /// Gets or sets the NewPassword /// + [JsonProperty("newPassword")] public string NewPassword { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs index 5963bba5..e455ed81 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs @@ -12,12 +12,20 @@ namespace PepperDash.Essentials.AppServer.Messengers { private readonly ITemperatureSensor device; + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public ITemperatureSensorMessenger(string key, ITemperatureSensor device, string messagePath) : base(key, messagePath, device as IKeyName) { this.device = device; } + + /// protected override void RegisterActions() { base.RegisterActions(); @@ -72,4 +80,4 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("temperatureInCelsius")] public bool TemperatureInCelsius { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs index 71c6349f..34f2db13 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs @@ -13,6 +13,13 @@ namespace PepperDash.Essentials.AppServer.Messengers { private ILightingScenes lightingScenesDevice; + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// public ILightingScenesMessenger(string key, ILightingScenes device, string messagePath) : base(key, messagePath, device as IKeyName) { @@ -31,6 +38,7 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(state); } + /// protected override void RegisterActions() { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index 3031f4ba..e634631a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -403,4 +403,4 @@ namespace PepperDash.Essentials.AppServer.Messengers } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs index b076566b..17eb2656 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs @@ -119,4 +119,4 @@ namespace PepperDash.Essentials.AppServer.Messengers timerHandler(deviceKey, action); } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs index 2e10d0f3..8969dcd3 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs @@ -6,87 +6,92 @@ using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Room.Config; -namespace PepperDash.Essentials.AppServer.Messengers +namespace PepperDash.Essentials.AppServer.Messengers; + +/// +/// Represents a RoomEventScheduleMessenger +/// +public class RoomEventScheduleMessenger : MessengerBase +{ + private readonly IRoomEventSchedule _room; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public RoomEventScheduleMessenger(string key, string messagePath, IRoomEventSchedule room) + : base(key, messagePath, room as IKeyName) + { + _room = room; + } + + #region Overrides of MessengerBase + + /// + protected override void RegisterActions() + { + AddAction("/saveScheduledEvents", (id, content) => SaveScheduledEvents(content.ToObject>())); + AddAction("/status", (id, content) => + { + var events = _room.GetScheduledEvents(); + + SendFullStatus(events, id); + }); + + AddAction("/scheduledEventsStatus", (id, content) => + { + var events = _room.GetScheduledEvents(); + + SendFullStatus(events, id); + }); + + _room.ScheduledEventsChanged += (sender, args) => SendFullStatus(args.ScheduledEvents); + } + + #endregion + + private void SaveScheduledEvents(List events) + { + foreach (var evt in events) + { + SaveScheduledEvent(evt); + } + } + + private void SaveScheduledEvent(ScheduledEventConfig eventConfig) + { + try + { + _room.AddOrUpdateScheduledEvent(eventConfig); + } + catch (Exception ex) + { + this.LogException(ex, "Exception saving event"); + } + } + + private void SendFullStatus(List events, string id = null) + { + + var message = new RoomEventScheduleStateMessage + { + ScheduleEvents = events, + }; + + PostStatusMessage(message, id); + } +} + +/// +/// Represents a RoomEventScheduleStateMessage +/// +public class RoomEventScheduleStateMessage : DeviceStateMessageBase { /// - /// Represents a RoomEventScheduleMessenger + /// Gets or sets the ScheduleEvents /// - public class RoomEventScheduleMessenger : MessengerBase - { - private readonly IRoomEventSchedule _room; - - - public RoomEventScheduleMessenger(string key, string messagePath, IRoomEventSchedule room) - : base(key, messagePath, room as IKeyName) - { - _room = room; - } - - #region Overrides of MessengerBase - - protected override void RegisterActions() - { - AddAction("/saveScheduledEvents", (id, content) => SaveScheduledEvents(content.ToObject>())); - AddAction("/status", (id, content) => - { - var events = _room.GetScheduledEvents(); - - SendFullStatus(events, id); - }); - - AddAction("/scheduledEventsStatus", (id, content) => - { - var events = _room.GetScheduledEvents(); - - SendFullStatus(events, id); - }); - - _room.ScheduledEventsChanged += (sender, args) => SendFullStatus(args.ScheduledEvents); - } - - #endregion - - private void SaveScheduledEvents(List events) - { - foreach (var evt in events) - { - SaveScheduledEvent(evt); - } - } - - private void SaveScheduledEvent(ScheduledEventConfig eventConfig) - { - try - { - _room.AddOrUpdateScheduledEvent(eventConfig); - } - catch (Exception ex) - { - this.LogException(ex, "Exception saving event"); - } - } - - private void SendFullStatus(List events, string id = null) - { - - var message = new RoomEventScheduleStateMessage - { - ScheduleEvents = events, - }; - - PostStatusMessage(message, id); - } - } - - /// - /// Represents a RoomEventScheduleStateMessage - /// - public class RoomEventScheduleStateMessage : DeviceStateMessageBase - { - [JsonProperty("scheduleEvents")] - /// - /// Gets or sets the ScheduleEvents - /// - public List ScheduleEvents { get; set; } - } -} \ No newline at end of file + [JsonProperty("scheduleEvents")] + public List ScheduleEvents { get; set; } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs index 5492cc2b..dc5cbe6a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs @@ -12,12 +12,19 @@ namespace PepperDash.Essentials.AppServer.Messengers { private readonly IShadesOpenCloseStop device; + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public IShadesOpenCloseStopMessenger(string key, IShadesOpenCloseStop shades, string messagePath) : base(key, messagePath, shades as IKeyName) { device = shades; } + /// protected override void RegisterActions() { base.RegisterActions(); @@ -102,9 +109,16 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("middleButtonLabel", NullValueHandling = NullValueHandling.Ignore)] public string MiddleButtonLabel { get; set; } + /// + /// Gets or sets the IsOpen + /// [JsonProperty("isOpen", NullValueHandling = NullValueHandling.Ignore)] public bool? IsOpen { get; set; } + + /// + /// Gets or sets the IsClosed + /// [JsonProperty("isClosed", NullValueHandling = NullValueHandling.Ignore)] public bool? IsClosed { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs index 63869ae0..857bfa0f 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs @@ -82,6 +82,7 @@ namespace PepperDash.Essentials.AppServer.Messengers )); } + /// protected override void RegisterActions() { AddAction("/fullStatus", (id, content) => SendFullStatusMessage(id)); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs index 5147da4d..77a7cc95 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs @@ -12,6 +12,12 @@ namespace PepperDash.Essentials.AppServer.Messengers { private readonly TwoWayDisplayBase _display; + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public TwoWayDisplayBaseMessenger(string key, string messagePath, TwoWayDisplayBase display) : base(key, messagePath, display) { @@ -34,6 +40,7 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(messageObj, id); } + /// protected override void RegisterActions() { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs index a7a072ba..abb6de5e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs @@ -9,22 +9,22 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class MobileControlMessage : IMobileControlMessage { - [JsonProperty("type")] /// /// Gets or sets the Type /// + [JsonProperty("type")] public string Type { get; set; } - [JsonProperty("clientId")] /// /// Gets or sets the ClientId /// + [JsonProperty("clientId")] public string ClientId { get; set; } - [JsonProperty("content")] /// /// Gets or sets the Content /// + [JsonProperty("content")] public JToken Content { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlSimpleContent.cs b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlSimpleContent.cs index 71972fc3..96d80f87 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlSimpleContent.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlSimpleContent.cs @@ -1,16 +1,18 @@ using Newtonsoft.Json; -namespace PepperDash.Essentials.AppServer +namespace PepperDash.Essentials.AppServer; + + +/// +/// Represents a MobileControlSimpleContent +/// +public class MobileControlSimpleContent { /// - /// Represents a MobileControlSimpleContent + /// Gets or sets the Value /// - public class MobileControlSimpleContent - { - [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] - /// - /// Gets or sets the Value - /// - public T Value { get; set; } - } + [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] + + public T Value { get; set; } } + diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLVtcJoinMap.cs b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLVtcJoinMap.cs index b53424bd..67ed8877 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLVtcJoinMap.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLVtcJoinMap.cs @@ -1,3 +1,4 @@ + using PepperDash.Essentials.Core; @@ -553,4 +554,4 @@ namespace PepperDash.Essentials.AppServer { } } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.MobileControl/AuthorizationResponse.cs b/src/PepperDash.Essentials.MobileControl/AuthorizationResponse.cs index 7ba130bc..4636f68a 100644 --- a/src/PepperDash.Essentials.MobileControl/AuthorizationResponse.cs +++ b/src/PepperDash.Essentials.MobileControl/AuthorizationResponse.cs @@ -1,37 +1,38 @@ using Newtonsoft.Json; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + + +/// +/// Represents a AuthorizationResponse +/// +public class AuthorizationResponse { - /// - /// Represents a AuthorizationResponse - /// - public class AuthorizationResponse - { - - /// - /// Gets or sets the Authorized - /// - [JsonProperty("authorized")] - public bool Authorized { get; set; } - - - /// - /// Gets or sets the Reason - /// - [JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] - public string Reason { get; set; } = null; - } /// - /// Represents a AuthorizationRequest + /// Gets or sets the Authorized /// - public class AuthorizationRequest - { + [JsonProperty("authorized")] + public bool Authorized { get; set; } - /// - /// Gets or sets the GrantCode - /// - [JsonProperty("grantCode")] - public string GrantCode { get; set; } - } + + /// + /// Gets or sets the Reason + /// + [JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] + public string Reason { get; set; } = null; } + +/// +/// Represents a AuthorizationRequest +/// +public class AuthorizationRequest +{ + + /// + /// Gets or sets the GrantCode + /// + [JsonProperty("grantCode")] + public string GrantCode { get; set; } +} + diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs index 16a9bd40..1136c3c1 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs @@ -2,32 +2,33 @@ using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + + +/// +/// Represents a MobileControlAction +/// +public class MobileControlAction : IMobileControlAction { /// - /// Represents a MobileControlAction + /// Gets the Messenger /// - public class MobileControlAction : IMobileControlAction + 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) { - /// - /// 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; - Action = handler; - } + Messenger = messenger; + Action = handler; } } + diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs index 6147b14e..c08b9c9f 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs @@ -5,34 +5,26 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials -{ - /// - /// 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" }; - } +namespace PepperDash.Essentials; - /// - public override EssentialsDevice BuildDevice(DeviceConfig dc) +public class MobileControlDeviceFactory : EssentialsDeviceFactory +{ + public MobileControlDeviceFactory() + { + TypeNames = new List { "appserver", "mobilecontrol", "webserver" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + try { - try - { - var props = dc.Properties.ToObject(); - return new MobileControlSystemController(dc.Key, dc.Name, props); - } - catch (Exception e) - { - Debug.LogMessage(e, "Error building Mobile Control System Controller"); - return null; - } + var props = dc.Properties.ToObject(); + return new MobileControlSystemController(dc.Key, dc.Name, props); + } + catch (Exception e) + { + Debug.LogMessage(e, "Error building Mobile Control System Controller"); + return null; } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs index d0415287..a0b5ebb8 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs @@ -1,74 +1,75 @@ using System.Collections.Generic; using Newtonsoft.Json; +using PepperDash.Essentials; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +/// +/// Configuration class for sending data to Mobile Control Edge or a client using the Direct Server +/// +public class MobileControlEssentialsConfig : EssentialsConfig { /// - /// Configuration class for sending data to Mobile Control Edge or a client using the Direct Server + /// Current versions for the system /// - 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() - { - 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 (Info == null) - Info = new InfoConfig(); - - RuntimeInfo = new MobileControlRuntimeInfo(); - } - } + [JsonProperty("runtimeInfo")] + public MobileControlRuntimeInfo RuntimeInfo { get; set; } /// - /// Represents a MobileControlRuntimeInfo + /// Create Configuration for Mobile Control. Used as part of the data sent to a client /// - public class MobileControlRuntimeInfo + /// The base configuration + public MobileControlEssentialsConfig(EssentialsConfig config) + : base() { + 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; - /// - /// Gets or sets the PluginVersion - /// - [JsonProperty("pluginVersion")] - public string PluginVersion { get; set; } + if (Info == null) + Info = new InfoConfig(); - /// - /// Essentials Version - /// - [JsonProperty("essentialsVersion")] - public string EssentialsVersion { get; set; } - - /// - /// PepperDash Core Version - /// - [JsonProperty("pepperDashCoreVersion")] - public string PepperDashCoreVersion { get; set; } - - - /// - /// List of Plugins loaded on this system - /// - [JsonProperty("essentialsPlugins")] - public List EssentialsPlugins { get; set; } + RuntimeInfo = new MobileControlRuntimeInfo(); } -} \ No newline at end of file + +} + +/// +/// Represents a MobileControlRuntimeInfo +/// +public class MobileControlRuntimeInfo +{ + + /// + /// Gets or sets the PluginVersion + /// + [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; } + + + /// + /// List of Plugins loaded on this system + /// + [JsonProperty("essentialsPlugins")] + public List EssentialsPlugins { get; set; } +} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 3adcaf87..f0f8caa1 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -2510,4 +2510,4 @@ namespace PepperDash.Essentials CrestronConsole.ConsoleCommandResponse("Usage: mobilehttprequest:N get/post url\r"); } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs index 634255fe..3e2707a8 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs @@ -5,193 +5,126 @@ using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -namespace PepperDash.Essentials.RoomBridges +namespace PepperDash.Essentials.RoomBridges; + +/// +/// +/// +public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger { - /// - /// Base class for a Mobile Control Bridge that's used to control a room - /// - public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger + public event EventHandler UserCodeChanged; + + public event EventHandler UserPromptedForCode; + + public event EventHandler ClientJoined; + + public event EventHandler AppUrlChanged; + + public IMobileControl Parent { get; private set; } + + public string AppUrl { get; private set; } + public string UserCode { get; private set; } + + public string QrCodeUrl { get; protected set; } + + public string QrCodeChecksum { get; protected set; } + + public string McServerUrl { get; private set; } + + public abstract string RoomName { get; } + + public abstract string RoomKey { get; } + + protected MobileControlBridgeBase(string key, string messagePath) + : base(key, messagePath) { - /// - /// 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; + protected MobileControlBridgeBase(string key, string messagePath, IKeyName device) + : base(key, messagePath, device) + { + } - /// - /// Triggered when a client joins to control this room - /// - public event EventHandler ClientJoined; + /// + /// Set the parent. Does nothing else. Override to add functionality such + /// as adding actions to parent + /// + /// + public virtual void AddParent(IMobileControl parent) + { + Parent = parent; - /// - /// Triggered when the App URL for this room changes - /// - public event EventHandler AppUrlChanged; + McServerUrl = Parent.ClientAppUrl; + } - /// - /// Gets or sets the Parent - /// - public IMobileControl Parent { get; private set; } - - /// - /// Gets or sets the AppUrl - /// - public string AppUrl { get; private set; } - /// - /// Gets or sets the UserCode - /// - public string UserCode { get; private set; } - - /// - /// Gets or sets the QrCodeUrl - /// - public string QrCodeUrl { get; protected set; } - - /// - /// Gets or sets the QrCodeChecksum - /// - public string QrCodeChecksum { get; protected set; } - - /// - /// Gets or sets the McServerUrl - /// - 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) + /// + /// Sets the UserCode on the bridge object. Called from controller. A changed code will + /// fire method UserCodeChange. Override that to handle changes + /// + /// + public void SetUserCode(string code) + { + var changed = UserCode != code; + UserCode = code; + if (changed) { - } - - /// - /// 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) - { - } - - /// - /// Set the parent. Does nothing else. Override to add functionality such - /// as adding actions to parent - /// - /// - /// - /// AddParent method - /// - public virtual void AddParent(IMobileControl parent) - { - Parent = parent; - - McServerUrl = Parent.ClientAppUrl; - } - - /// - /// Sets the UserCode on the bridge object. Called from controller. A changed code will - /// fire method UserCodeChange. Override that to handle changes - /// - /// - /// - /// SetUserCode method - /// - public void SetUserCode(string code) - { - var changed = UserCode != code; - UserCode = code; - if (changed) - { - UserCodeChange(); - } - } - - - /// - /// Sets the UserCode on the bridge object. Called from controller. A changed code will - /// fire method UserCodeChange. Override that to handle changes - /// - /// - /// Checksum of the QR code. Used for Cisco codec branding command - public void SetUserCode(string code, string qrChecksum) - { - QrCodeChecksum = qrChecksum; - - SetUserCode(code); - } - - /// - /// Update the App Url with the provided URL - /// - /// The new App URL - public virtual void UpdateAppUrl(string url) - { - AppUrl = url; - - var handler = AppUrlChanged; - - if (handler == null) return; - - handler(this, new EventArgs()); - } - - /// - /// Empty method in base class. Override this to add functionality - /// when code changes - /// - protected virtual void UserCodeChange() - { - this.LogDebug("Server user code changed: {userCode}", UserCode); - - var qrUrl = string.Format($"{Parent.Host}/api/rooms/{Parent.SystemUuid}/{RoomKey}/qr?x={new Random().Next()}"); - QrCodeUrl = qrUrl; - - this.LogDebug("Server user code changed: {userCode} - {qrCodeUrl}", UserCode, qrUrl); - - 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()); + UserCodeChange(); } } + + + /// + /// Sets the UserCode on the bridge object. Called from controller. A changed code will + /// fire method UserCodeChange. Override that to handle changes + /// + /// + /// Checksum of the QR code. Used for Cisco codec branding command + public void SetUserCode(string code, string qrChecksum) + { + QrCodeChecksum = qrChecksum; + + SetUserCode(code); + } + + public virtual void UpdateAppUrl(string url) + { + AppUrl = url; + + var handler = AppUrlChanged; + + if (handler == null) return; + + handler(this, new EventArgs()); + } + + /// + /// Empty method in base class. Override this to add functionality + /// when code changes + /// + protected virtual void UserCodeChange() + { + this.LogDebug("Server user code changed: {userCode}", UserCode); + + var qrUrl = string.Format($"{Parent.Host}/api/rooms/{Parent.SystemUuid}/{RoomKey}/qr?x={new Random().Next()}"); + QrCodeUrl = qrUrl; + + this.LogDebug("Server user code changed: {userCode} - {qrCodeUrl}", UserCode, qrUrl); + + OnUserCodeChanged(); + } + + protected void OnUserCodeChanged() + { + UserCodeChanged?.Invoke(this, new EventArgs()); + } + + protected void OnUserPromptedForCode() + { + UserPromptedForCode?.Invoke(this, new EventArgs()); + } + + protected void OnClientJoined() + { + ClientJoined?.Invoke(this, new EventArgs()); + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs index 508fb776..f3b02bb6 100644 --- a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs +++ b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs @@ -3,89 +3,74 @@ using System.Net.Http; using System.Threading.Tasks; using PepperDash.Core; -namespace PepperDash.Essentials.Services +namespace PepperDash.Essentials.Services; + + +public class MobileControlApiService { + private readonly HttpClient _client; - /// - /// Service for interacting with a Mobile Control Edge server instance - /// - public class MobileControlApiService + public MobileControlApiService(string apiUrl) { - private readonly HttpClient _client; - - /// - /// Create an instance of the class. - /// - /// Mobile Control Edge API URL - public MobileControlApiService(string apiUrl) + var handler = new HttpClientHandler { - var handler = new HttpClientHandler + AllowAutoRedirect = false, + ServerCertificateCustomValidationCallback = (req, cert, certChain, errors) => true + }; + + _client = new HttpClient(handler); + } + + public async Task SendAuthorizationRequest(string apiUrl, string grantCode, string systemUuid) + { + try + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{apiUrl}/system/{systemUuid}/authorize?grantCode={grantCode}"); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Sending authorization request to {host}", null, request.RequestUri); + + var response = await _client.SendAsync(request); + + var authResponse = new AuthorizationResponse { - AllowAutoRedirect = false, - ServerCertificateCustomValidationCallback = (req, cert, certChain, errors) => true + Authorized = response.StatusCode == System.Net.HttpStatusCode.OK }; - _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 + if (authResponse.Authorized) { - var request = new HttpRequestMessage(HttpMethod.Get, $"{apiUrl}/system/{systemUuid}/authorize?grantCode={grantCode}"); + return authResponse; + } - Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Sending authorization request to {host}", null, request.RequestUri); + if (response.StatusCode == System.Net.HttpStatusCode.Moved) + { + var location = response.Headers.Location; - var response = await _client.SendAsync(request); - - var authResponse = new AuthorizationResponse - { - Authorized = response.StatusCode == System.Net.HttpStatusCode.OK - }; - - if (authResponse.Authorized) - { - return authResponse; - } - - if (response.StatusCode == System.Net.HttpStatusCode.Moved) - { - var location = response.Headers.Location; - - authResponse.Reason = $"ERROR: Mobile Control API has moved. Please adjust configuration to \"{location}\""; - - return authResponse; - } - - var responseString = await response.Content.ReadAsStringAsync(); - - switch (responseString) - { - case "codeNotFound": - authResponse.Reason = $"Authorization failed. Code not found for system UUID {systemUuid}"; - break; - case "uuidNotFound": - authResponse.Reason = $"Authorization failed. System UUID {systemUuid} not found. Check Essentials configuration."; - break; - default: - authResponse.Reason = $"Authorization failed. Response {response.StatusCode}: {responseString}"; - break; - } + authResponse.Reason = $"ERROR: Mobile Control API has moved. Please adjust configuration to \"{location}\""; return authResponse; } - catch (Exception ex) + + var responseString = await response.Content.ReadAsStringAsync(); + + switch (responseString) { - Debug.LogMessage(ex, "Error authorizing with Mobile Control"); - return new AuthorizationResponse { Authorized = false, Reason = ex.Message }; + case "codeNotFound": + authResponse.Reason = $"Authorization failed. Code not found for system UUID {systemUuid}"; + break; + case "uuidNotFound": + authResponse.Reason = $"Authorization failed. System UUID {systemUuid} not found. Check Essentials configuration."; + break; + default: + authResponse.Reason = $"Authorization failed. Response {response.StatusCode}: {responseString}"; + break; } + + return authResponse; + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error authorizing with Mobile Control"); + return new AuthorizationResponse { Authorized = false, Reason = ex.Message }; } } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs index c7efbf4c..827331db 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs @@ -1,21 +1,21 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Touchpanel +namespace PepperDash.Essentials.Touchpanel; + +/// +/// Defines the contract for ITheme +/// +public interface ITheme : IKeyed { /// - /// Defines the contract for ITheme + /// Current theme /// - public interface ITheme : IKeyed - { - /// - /// Current theme - /// - string Theme { get; } + string Theme { get; } - /// - /// Set the theme with the given value - /// - /// The theme to set - void UpdateTheme(string theme); - } + /// + /// 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 055ad80c..35a7d37e 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs @@ -1,52 +1,53 @@ using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Touchpanel +namespace PepperDash.Essentials.Touchpanel; + + +/// +/// Defines the contract for ITswAppControl +/// +public interface ITswAppControl : IKeyed { /// - /// Defines the contract for ITswAppControl + /// Updates when the Zoom Room Control Application opens or closes /// - 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(); - } + BoolFeedback AppOpenFeedback { get; } /// - /// Defines the contract for ITswZoomControl + /// Hide the Zoom App and show the User Control Application /// - public interface ITswZoomControl : IKeyed - { - /// - /// Updates when Zoom has an incoming call - /// - BoolFeedback ZoomIncomingCallFeedback { get; } + void HideOpenApp(); - /// - /// Updates when Zoom is in a call - /// - BoolFeedback ZoomInCallFeedback { get; } + /// + /// Close the Zoom App and show the User Control Application + /// + void CloseOpenApp(); - /// - /// End a Zoom Call - /// - void EndZoomCall(); - } + /// + /// Open the Zoom App + /// + void OpenApp(); } + +/// +/// Defines the contract for ITswZoomControl +/// +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 acfcab2a..a4e7739e 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs @@ -72,4 +72,4 @@ namespace PepperDash.Essentials.Touchpanel [JsonProperty("appOpen", NullValueHandling = NullValueHandling.Ignore)] public bool? AppOpen { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs index bbf4030e..61a3ebbd 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs @@ -92,4 +92,4 @@ namespace PepperDash.Essentials.Touchpanel [JsonProperty("incomingCall", NullValueHandling = NullValueHandling.Ignore)] public bool? IncomingCall { get; set; } } -} +} \ 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 5830782d..30f4542b 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -818,4 +818,4 @@ namespace PepperDash.Essentials.Touchpanel } } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs index 8156834b..cf022c16 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs @@ -1,39 +1,40 @@ -using Newtonsoft.Json; + +using Newtonsoft.Json; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Touchpanel +namespace PepperDash.Essentials.Touchpanel; + +/// +/// Represents a MobileControlTouchpanelProperties +/// +public class MobileControlTouchpanelProperties : CrestronTouchpanelPropertiesConfig { + /// - /// Represents a MobileControlTouchpanelProperties + /// Gets or sets the UseDirectServer /// - public class MobileControlTouchpanelProperties : CrestronTouchpanelPropertiesConfig - { - - /// - /// Gets or sets the UseDirectServer - /// - [JsonProperty("useDirectServer")] - public bool UseDirectServer { get; set; } = false; + [JsonProperty("useDirectServer")] + public bool UseDirectServer { get; set; } = false; - /// - /// Gets or sets the ZoomRoomController - /// - [JsonProperty("zoomRoomController")] - public bool ZoomRoomController { get; set; } = false; + /// + /// Gets or sets the ZoomRoomController + /// + [JsonProperty("zoomRoomController")] + public bool ZoomRoomController { get; set; } = false; - /// - /// Gets or sets the ButtonToolbarTimoutInS - /// - [JsonProperty("buttonToolbarTimeoutInS")] - public ushort ButtonToolbarTimoutInS { get; set; } = 0; + /// + /// Gets or sets the ButtonToolbarTimoutInS + /// + [JsonProperty("buttonToolbarTimeoutInS")] + public ushort ButtonToolbarTimoutInS { get; set; } = 0; - /// - /// Gets or sets the Theme - /// - [JsonProperty("theme")] - public string Theme { get; set; } = "light"; - } -} \ No newline at end of file + /// + /// Gets or sets the Theme + /// + [JsonProperty("theme")] + public string Theme { get; set; } = "light"; +} + diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs index 35d85442..cecd613e 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -57,4 +57,4 @@ namespace PepperDash.Essentials.Touchpanel [JsonProperty("theme")] public string Theme { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs b/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs index ce31f766..a7f4e893 100644 --- a/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs +++ b/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs @@ -1,24 +1,24 @@ using Newtonsoft.Json; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +/// +/// Represents a UserCodeChangedContent +/// +public class UserCodeChangedContent { + /// - /// Represents a UserCodeChangedContent + /// Gets or sets the UserCode /// - public class UserCodeChangedContent - { - - /// - /// Gets or sets the UserCode - /// - [JsonProperty("userCode")] - public string UserCode { get; set; } + [JsonProperty("userCode")] + public string UserCode { get; set; } - /// - /// Gets or sets the QrChecksum - /// - [JsonProperty("qrChecksum", NullValueHandling = NullValueHandling.Include)] - public string QrChecksum { get; set; } - } + /// + /// Gets or sets the QrChecksum + /// + [JsonProperty("qrChecksum", NullValueHandling = NullValueHandling.Include)] + public string QrChecksum { get; set; } } + diff --git a/src/PepperDash.Essentials.MobileControl/Volumes.cs b/src/PepperDash.Essentials.MobileControl/Volumes.cs index 44febbfc..2d95a675 100644 --- a/src/PepperDash.Essentials.MobileControl/Volumes.cs +++ b/src/PepperDash.Essentials.MobileControl/Volumes.cs @@ -1,137 +1,137 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + + +/// +/// Represents a Volumes +/// +public class Volumes +{ + + /// + /// Gets or sets the Master + /// + [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; } +} + +/// +/// Represents a Volume +/// +public class Volume { /// - /// Represents a Volumes + /// Gets or sets the Key /// - public class Volumes + [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; } + + + /// + /// Gets or sets the Label + /// + [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; } + + + + /// + /// Gets or sets the MuteIcon + /// + [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) { - - /// - /// Gets or sets the Master - /// - [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; } + Level = level; + Muted = muted; + Label = label; + HasMute = hasMute; + MuteIcon = muteIcon; } /// - /// Represents a Volume + /// Create an instance of the class /// - public class Volume + /// The key for this volume object + /// The level for this volume object + public Volume(string key, int level) + : this(key) { - /// - /// 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; } - - - /// - /// Gets or sets the Label - /// - [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; } - - - - /// - /// Gets or sets the MuteIcon - /// - [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) - { - Level = level; - Muted = muted; - Label = label; - HasMute = hasMute; - 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; - } + Level = level; } -} \ No newline at end of file + + /// + /// 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 1c204e8e..5bf7be4b 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs @@ -81,4 +81,4 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonProperty("path")] public string Path { get; set; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs index 5d9e9766..850a3e6a 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs @@ -68,4 +68,4 @@ namespace PepperDash.Essentials.WebApiHandlers } } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index 676fd3c8..551a9b4d 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -284,4 +284,4 @@ namespace PepperDash.Essentials.WebApiHandlers this.client = client; } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs index 2f972ddf..2a7ed8c7 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -1,3 +1,4 @@ + using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 210dc577..f1af6bbf 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -1,26 +1,29 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Net.Http; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; -using PepperDash.Essentials.Core; -using System.Collections.Generic; -using System; -using System.Net.Http; using Crestron.SimplSharp; -using System.Linq; -using PepperDash.Core.Logging; -using PepperDash.Essentials.Core.Web; using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using Org.BouncyCastle.Crypto.Prng; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Web; +using PepperDash.Essentials.RoomBridges; using PepperDash.Essentials.WebApiHandlers; +using Serilog.Events; using WebSocketSharp; using WebSocketSharp.Net; using WebSocketSharp.Server; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System.IO; -using Newtonsoft.Json; -using PepperDash.Core; -using Serilog.Events; -using PepperDash.Essentials.RoomBridges; + namespace PepperDash.Essentials.WebSocketServer { @@ -48,7 +51,7 @@ namespace PepperDash.Essentials.WebSocketServer private HttpServer _server; /// - /// Gets the WebSocketServer instance + /// Gets the HttpServer instance /// public HttpServer Server => _server; @@ -264,35 +267,34 @@ namespace PepperDash.Essentials.WebSocketServer { base.Initialize(); - _server = new HttpServer(Port, false); - - - - _server.OnGet += (sender, e) => Server_OnGet(sender, e); - - _server.OnOptions += (sender, e) => Server_OnOptions(sender, e); - - if (_parent.Config.DirectServer.Logging.EnableRemoteLogging) - { - _server.OnPost += (sender, e) => Server_OnPost(sender, e); - } + ServerSslConfiguration sslConfig = null; if (_parent.Config.DirectServer.Secure) { this.LogInformation("Adding SSL Configuration to server"); - - ServerSslConfiguration sslConfig = null; - sslConfig = new ServerSslConfiguration(new X509Certificate2($"\\user\\{certificateName}.pfx", certificatePassword)) { ClientCertificateRequired = false, CheckCertificateRevocation = false, EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 }; + } + _server = new HttpServer(Port, false); + if (sslConfig != null) + { _server.SslConfiguration.ServerCertificate = sslConfig.ServerCertificate; } + _server.OnGet += Server_OnGet; + + _server.OnOptions += Server_OnOptions; + + if (_parent.Config.DirectServer.Logging.EnableRemoteLogging) + { + _server.OnPost += Server_OnPost; + } + _server.Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); // setting to trace to allow logging level to be controlled by appdebug @@ -1463,4 +1465,4 @@ namespace PepperDash.Essentials.WebSocketServer } } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 607323ba..99e25634 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -267,4 +267,4 @@ namespace PepperDash.Essentials.WebSocketServer this.LogDebug(e.Exception, "Stack Trace"); } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs index 3dd67bd8..566ed22a 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs @@ -52,4 +52,4 @@ namespace PepperDash.Essentials.WebSocketServer } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs b/src/PepperDash.Essentials/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs index 9c78c4aa..b6533089 100644 --- a/src/PepperDash.Essentials/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs +++ b/src/PepperDash.Essentials/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs @@ -3,38 +3,37 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -namespace PepperDash.Essentials.Fusion +namespace PepperDash.Essentials.Fusion; + +public class EssentialsHuddleSpaceFusionSystemControllerBase { - public class EssentialsHuddleSpaceFusionSystemControllerBase + private ClientWebSocket _webSocket; + + public EssentialsHuddleSpaceFusionSystemControllerBase() { - private ClientWebSocket _webSocket; + _webSocket = new ClientWebSocket(); + } - public EssentialsHuddleSpaceFusionSystemControllerBase() - { - _webSocket = new ClientWebSocket(); - } + public async Task ConnectAsync(Uri uri) + { + await _webSocket.ConnectAsync(uri, CancellationToken.None); + } - public async Task ConnectAsync(Uri uri) - { - await _webSocket.ConnectAsync(uri, CancellationToken.None); - } + public async Task SendAsync(string message) + { + var buffer = System.Text.Encoding.UTF8.GetBytes(message); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } - public async Task SendAsync(string message) - { - var buffer = System.Text.Encoding.UTF8.GetBytes(message); - await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); - } + public async Task ReceiveAsync() + { + var buffer = new byte[1024]; + var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); + } - public async Task ReceiveAsync() - { - var buffer = new byte[1024]; - var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); - } - - public async Task CloseAsync() - { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } + public async Task CloseAsync() + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } } diff --git a/src/PepperDash.Essentials/HttpLogoServer.cs b/src/PepperDash.Essentials/HttpLogoServer.cs index d5d133e6..174ebd7e 100644 --- a/src/PepperDash.Essentials/HttpLogoServer.cs +++ b/src/PepperDash.Essentials/HttpLogoServer.cs @@ -7,127 +7,117 @@ using Crestron.SimplSharp.Net.Http; using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +public class HttpLogoServer { /// - /// HTTP server for serving logo images and files + /// /// - public class HttpLogoServer + readonly HttpServer _server; + + /// + /// + /// + readonly string _fileDirectory; + + /// + /// + /// + public static Dictionary ExtensionContentTypes; + + /// + /// + /// + /// + /// + public HttpLogoServer(int port, string directory) { - /// - /// The HTTP server instance - /// - readonly HttpServer _server; - - /// - /// The directory containing files to serve - /// - readonly string _fileDirectory; - - /// - /// Dictionary mapping file extensions to content types - /// - public static Dictionary ExtensionContentTypes; - - /// - /// Initializes a new instance of the HttpLogoServer class - /// - /// Port number for the HTTP server - /// Directory containing files to serve - public HttpLogoServer(int port, string directory) - { - ExtensionContentTypes = new Dictionary + ExtensionContentTypes = new Dictionary { - //{ ".css", "text/css" }, - //{ ".htm", "text/html" }, - //{ ".html", "text/html" }, + //{ ".css", "text/css" }, + //{ ".htm", "text/html" }, + //{ ".html", "text/html" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" }, - //{ ".js", "application/javascript" }, - //{ ".json", "application/json" }, - //{ ".map", "application/x-navimap" }, + //{ ".js", "application/javascript" }, + //{ ".json", "application/json" }, + //{ ".map", "application/x-navimap" }, { ".pdf", "application/pdf" }, { ".png", "image/png" }, - //{ ".txt", "text/plain" }, + //{ ".txt", "text/plain" }, }; - _server = new HttpServer {Port = port}; - _fileDirectory = directory; - _server.OnHttpRequest += Server_OnHttpRequest; - _server.Open(); + _server = new HttpServer {Port = port}; + _fileDirectory = directory; + _server.OnHttpRequest += Server_OnHttpRequest; + _server.Open(); - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - } + CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; + } - /// - /// Handles incoming HTTP requests and serves files from the configured directory - /// - /// The HTTP server instance - /// HTTP request arguments - void Server_OnHttpRequest(object sender, OnHttpRequestArgs args) + /// + /// + /// + void Server_OnHttpRequest(object sender, OnHttpRequestArgs args) + { + var path = args.Request.Path; + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "HTTP Request with path: '{requestPath:l}'", args.Request.Path); + + try { - var path = args.Request.Path; - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "HTTP Request with path: '{requestPath:l}'", args.Request.Path); - - try + if (File.Exists(_fileDirectory + path)) { - if (File.Exists(_fileDirectory + path)) - { - var filePath = path.Replace('/', '\\'); - var localPath = string.Format(@"{0}{1}", _fileDirectory, filePath); + var filePath = path.Replace('/', '\\'); + var localPath = string.Format(@"{0}{1}", _fileDirectory, filePath); - Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server attempting to find file: '{localPath:l}'", localPath); - if (File.Exists(localPath)) - { - args.Response.Header.ContentType = GetContentType(new FileInfo(localPath).Extension); - args.Response.ContentStream = new FileStream(localPath, FileMode.Open, FileAccess.Read); - } - else - { - Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server Cannot find file '{localPath:l}'", localPath); - args.Response.ContentString = string.Format("Not found: '{0}'", filePath); - args.Response.Code = 404; - } + Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server attempting to find file: '{localPath:l}'", localPath); + if (File.Exists(localPath)) + { + args.Response.Header.ContentType = GetContentType(new FileInfo(localPath).Extension); + args.Response.ContentStream = new FileStream(localPath, FileMode.Open, FileAccess.Read); } else { - Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server: '{file:l}' does not exist", _fileDirectory + path); - args.Response.ContentString = string.Format("Not found: '{0}'", _fileDirectory + path); + Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server Cannot find file '{localPath:l}'", localPath); + args.Response.ContentString = string.Format("Not found: '{0}'", filePath); args.Response.Code = 404; } } - catch (Exception ex) + else { - Debug.LogMessage(LogEventLevel.Error, "Exception getting file: {exception}", ex.Message, ex.StackTrace); - Debug.LogMessage(LogEventLevel.Verbose, "Stack Trace: {stackTrace}", ex.StackTrace); - - args.Response.Code = 400; - args.Response.ContentString = string.Format("invalid request"); + Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server: '{file:l}' does not exist", _fileDirectory + path); + args.Response.ContentString = string.Format("Not found: '{0}'", _fileDirectory + path); + args.Response.Code = 404; } } - - /// - /// Handles program status events and closes the server when the program is stopping - /// - /// The program status event type - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + catch (Exception ex) { - if (programEventType == eProgramStatusEventType.Stopping) - _server.Close(); - } + Debug.LogMessage(LogEventLevel.Error, "Exception getting file: {exception}", ex.Message, ex.StackTrace); + Debug.LogMessage(LogEventLevel.Verbose, "Stack Trace: {stackTrace}", ex.StackTrace); - /// - /// Gets the content type for a file based on its extension - /// - /// The file extension - /// The corresponding content type string - /// - /// GetContentType method - /// - public static string GetContentType(string extension) - { - var type = ExtensionContentTypes.ContainsKey(extension) ? ExtensionContentTypes[extension] : "text/plain"; - return type; + args.Response.Code = 400; + args.Response.ContentString = string.Format("invalid request"); } } + + /// + /// + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + _server.Close(); + } + + /// + /// + /// + /// + /// + public static string GetContentType(string extension) + { + var type = ExtensionContentTypes.ContainsKey(extension) ? ExtensionContentTypes[extension] : "text/plain"; + return type; + } } \ No newline at end of file From 76759d35cc679c44de0a6a4ad2b4bb349b76d050 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 16:03:00 -0500 Subject: [PATCH 014/161] feat: remove context file for storing values --- src/PepperDash.Core/Logging/Debug.cs | 1699 ++++++++--------- .../Logging/DebugWebsocketSink.cs | 576 +++--- 2 files changed, 1030 insertions(+), 1245 deletions(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 6e4fb37d..b3e7452a 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -22,95 +22,88 @@ using Serilog.Templates; namespace PepperDash.Core; - /// - /// - public static class Debug +/// +/// Contains debug commands for use in various situations +/// +public static class Debug +{ + private static readonly string LevelStoreKey = "ConsoleDebugLevel"; + private static readonly string WebSocketLevelStoreKey = "WebsocketDebugLevel"; + private static readonly string ErrorLogLevelStoreKey = "ErrorLogDebugLevel"; + private static readonly string FileLevelStoreKey = "FileDebugLevel"; + private static readonly string DoNotLoadOnNextBootKey = "DoNotLoadOnNextBoot"; + + private static readonly Dictionary _logLevels = new Dictionary() { - private static readonly string LevelStoreKey = "ConsoleDebugLevel"; - private static readonly string WebSocketLevelStoreKey = "WebsocketDebugLevel"; - private static readonly string ErrorLogLevelStoreKey = "ErrorLogDebugLevel"; - private static readonly string FileLevelStoreKey = "FileDebugLevel"; + {0, LogEventLevel.Information }, + {3, LogEventLevel.Warning }, + {4, LogEventLevel.Error }, + {5, LogEventLevel.Fatal }, + {1, LogEventLevel.Debug }, + {2, LogEventLevel.Verbose }, + }; - private static readonly Dictionary _logLevels = new Dictionary() - { - {0, LogEventLevel.Information }, - {3, LogEventLevel.Warning }, - {4, LogEventLevel.Error }, - {5, LogEventLevel.Fatal }, - {1, LogEventLevel.Debug }, - {2, LogEventLevel.Verbose }, - }; + private static ILogger _logger; - private static ILogger _logger; + private static readonly LoggingLevelSwitch _consoleLoggingLevelSwitch; - private static readonly LoggingLevelSwitch _consoleLogLevelSwitch; + private static readonly LoggingLevelSwitch _websocketLoggingLevelSwitch; - private static readonly LoggingLevelSwitch _websocketLogLevelSwitch; + private static readonly LoggingLevelSwitch _errorLogLevelSwitch; - private static readonly LoggingLevelSwitch _errorLogLevelSwitch; + private static readonly LoggingLevelSwitch _fileLevelSwitch; - private static readonly LoggingLevelSwitch _fileLogLevelSwitch; + public static LogEventLevel WebsocketMinimumLogLevel + { + get { return _websocketLoggingLevelSwitch.MinimumLevel; } + } - /// - /// Gets the minimum log level for the websocket sink. - /// - public static LogEventLevel WebsocketMinimumLogLevel - { - get { return _websocketLogLevelSwitch.MinimumLevel; } - } + private static readonly DebugWebsocketSink _websocketSink; - private static readonly DebugWebsocketSink _websocketSink; + public static DebugWebsocketSink WebsocketSink + { + get { return _websocketSink; } + } - /// - /// Gets the websocket sink for debug logging. - /// - public static DebugWebsocketSink WebsocketSink - { - get { return _websocketSink; } - } + /// + /// Describes the folder location where a given program stores it's debug level memory. By default, the + /// file written will be named appNdebug where N is 1-10. + /// + public static string OldFilePathPrefix = @"\nvram\debug\"; - /// - /// Describes the folder location where a given program stores it's debug level memory. By default, the - /// file written will be named appNdebug where N is 1-10. - /// - public static string OldFilePathPrefix = @"\nvram\debug\"; + /// + /// Describes the new folder location where a given program stores it's debug level memory. By default, the + /// file written will be named appNdebug where N is 1-10. + /// + public static string NewFilePathPrefix = @"\user\debug\"; - /// - /// Describes the new folder location where a given program stores it's debug level memory. By default, the - /// file written will be named appNdebug where N is 1-10. - /// - public static string NewFilePathPrefix = @"\user\debug\"; + /// + /// The name of the file containing the current debug settings. + /// + public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber); - /// - /// The name of the file containing the current debug settings. - /// - public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber); + /// + /// Debug level to set for a given program. + /// + public static int Level { get; private set; } - /// - /// Gets or sets the Level - /// - public static int Level { get; private set; } + /// + /// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal + /// + public static bool DoNotLoadConfigOnNextBoot { get; private set; } - /// - /// Gets or sets the DoNotLoadConfigOnNextBoot - /// - public static bool DoNotLoadConfigOnNextBoot { get; private set; } + private static DebugContextCollection _contexts; - private static DebugContextCollection _contexts; + private const int SaveTimeoutMs = 30000; - private const int SaveTimeoutMs = 30000; + public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; - /// - /// Indicates whether the system is running on an appliance. - /// - public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; + /// + /// Version for the currently loaded PepperDashCore dll + /// + public static string PepperDashCoreVersion { get; private set; } - /// - /// Gets or sets the PepperDashCoreVersion - /// - public static string PepperDashCoreVersion { get; private set; } - - private static CTimer _saveTimer; + private static CTimer _saveTimer; /// /// When true, the IncludedExcludedKeys dict will contain keys to include. @@ -120,16 +113,15 @@ namespace PepperDash.Core; private static readonly Dictionary IncludedExcludedKeys; - private static readonly LoggerConfiguration _defaultLoggerConfiguration; + private static readonly LoggerConfiguration _defaultLoggerConfiguration; - private static LoggerConfiguration _loggerConfiguration; + private static LoggerConfiguration _loggerConfiguration; - /// - /// Gets the logger configuration for the debug logging. - /// - public static LoggerConfiguration LoggerConfiguration => _loggerConfiguration; + public static LoggerConfiguration LoggerConfiguration => _loggerConfiguration; - static Debug() + static Debug() + { + try { CrestronDataStoreStatic.InitCrestronDataStore(); @@ -141,13 +133,13 @@ namespace PepperDash.Core; var defaultFileLogLevel = GetStoredLogEventLevel(FileLevelStoreKey); - _consoleLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultConsoleLevel); + _consoleLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultConsoleLevel); - _websocketLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultWebsocketLevel); + _websocketLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultWebsocketLevel); _errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel); - _fileLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); + _fileLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); _websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); @@ -165,31 +157,16 @@ namespace PepperDash.Core; .MinimumLevel.Verbose() .Enrich.FromLogContext() .Enrich.With(new CrestronEnricher()) - .WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: _consoleLogLevelSwitch) - .WriteTo.Sink(_websocketSink, levelSwitch: _websocketLogLevelSwitch) + .WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: _consoleLoggingLevelSwitch) + .WriteTo.Sink(_websocketSink, levelSwitch: _websocketLoggingLevelSwitch) .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: _errorLogLevelSwitch) .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug, - retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 7 : 14, - levelSwitch: _fileLogLevelSwitch + retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, + levelSwitch: _fileLevelSwitch ); - try - { - if (InitialParametersClass.NumberOfRemovableDrives > 0) - { - CrestronConsole.PrintLine("{0} RM Drive(s) Present. Initializing CrestronLogger", InitialParametersClass.NumberOfRemovableDrives); - _defaultLoggerConfiguration.WriteTo.Sink(new DebugCrestronLoggerSink()); - } - else - CrestronConsole.PrintLine("No RM Drive(s) Present. Not using Crestron Logger"); - } - catch (Exception e) - { - CrestronConsole.PrintLine("Initializing of CrestronLogger failed: {0}", e); - } - // Instantiate the root logger _loggerConfiguration = _defaultLoggerConfiguration; @@ -231,422 +208,327 @@ namespace PepperDash.Core; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - LoadMemory(); - - var context = _contexts.GetOrCreateItem("DEFAULT"); - Level = context.Level; - DoNotLoadConfigOnNextBoot = context.DoNotLoadOnNextBoot; + DoNotLoadConfigOnNextBoot = GetDoNotLoadOnNextBoot(); if (DoNotLoadConfigOnNextBoot) CrestronConsole.PrintLine(string.Format("Program {0} will not load config after next boot. Use console command go:{0} to load the config manually", InitialParametersClass.ApplicationNumber)); - _errorLogLevelSwitch.MinimumLevelChanged += (sender, args) => + _consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) => { - LogMessage(LogEventLevel.Information, "Error log debug level set to {minimumLevel}", _errorLogLevelSwitch.MinimumLevel); + LogMessage(LogEventLevel.Information, "Console debug level set to {minimumLevel}", _consoleLoggingLevelSwitch.MinimumLevel); }; + } + catch (Exception ex) + { + LogError(ex, "Exception in Debug static constructor: {message}", ex.Message); + } + } - // Set initial error log level based on platform && stored level. If appliance, use stored level, otherwise default to verbose - SetErrorLogMinimumDebugLevel(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? _errorLogLevelSwitch.MinimumLevel : LogEventLevel.Verbose); + private static bool GetDoNotLoadOnNextBoot() + { + var err = CrestronDataStoreStatic.GetLocalBoolValue(DoNotLoadOnNextBootKey, out var doNotLoad); + + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + { + LogError("Error retrieving DoNotLoadOnNextBoot value: {err}", err); + doNotLoad = false; } - /// - /// UpdateLoggerConfiguration method - /// - public static void UpdateLoggerConfiguration(LoggerConfiguration config) + return doNotLoad; + } + + public static void UpdateLoggerConfiguration(LoggerConfiguration config) + { + _loggerConfiguration = config; + + _logger = config.CreateLogger(); + } + + public static void ResetLoggerConfiguration() + { + _loggerConfiguration = _defaultLoggerConfiguration; + + _logger = _loggerConfiguration.CreateLogger(); + } + + private static LogEventLevel GetStoredLogEventLevel(string levelStoreKey) + { + try { - _loggerConfiguration = config; + var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel); - _logger = config.CreateLogger(); - } - - /// - /// ResetLoggerConfiguration method - /// - public static void ResetLoggerConfiguration() - { - _loggerConfiguration = _defaultLoggerConfiguration; - - _logger = _loggerConfiguration.CreateLogger(); - } - - private static LogEventLevel GetStoredLogEventLevel(string levelStoreKey) - { - try + if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) { - var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel); - - if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - { - CrestronConsole.Print($"Unable to retrieve stored log level for {levelStoreKey}.\r\nError: {result}.\r\nSetting level to {LogEventLevel.Information}\r\n"); - - CrestronDataStoreStatic.SetLocalIntValue(levelStoreKey, levelStoreKey == ErrorLogLevelStoreKey ? (int)LogEventLevel.Warning : (int)LogEventLevel.Information); - - return levelStoreKey == ErrorLogLevelStoreKey ? LogEventLevel.Warning : LogEventLevel.Information; - } - - if (logLevel < 0 || logLevel > 5) - { - CrestronConsole.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); - return LogEventLevel.Information; - } - - CrestronConsole.PrintLine($"Stored log level for {levelStoreKey} is {logLevel}"); - - return (LogEventLevel)logLevel; - } - catch (Exception ex) - { - CrestronConsole.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); + CrestronConsole.Print($"Unable to retrieve stored log level for {levelStoreKey}.\r\nError: {result}.\r\nSetting level to {LogEventLevel.Information}\r\n"); return LogEventLevel.Information; } - } - private static void GetVersion() - { - var assembly = Assembly.GetExecutingAssembly(); - var ver = - assembly - .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); - - if (ver != null && ver.Length > 0) + if(logLevel < 0 || logLevel > 5) { - if (ver[0] is AssemblyInformationalVersionAttribute verAttribute) - { - PepperDashCoreVersion = verAttribute.InformationalVersion; - } + CrestronConsole.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); + return LogEventLevel.Information; } - else + + return (LogEventLevel)logLevel; + } catch (Exception ex) + { + CrestronConsole.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); + return LogEventLevel.Information; + } + } + + private static void GetVersion() + { + var assembly = Assembly.GetExecutingAssembly(); + var ver = + assembly + .GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), false); + + if (ver != null && ver.Length > 0) + { + if (ver[0] is AssemblyInformationalVersionAttribute verAttribute) { - var version = assembly.GetName().Version; - PepperDashCoreVersion = version.ToString(); + PepperDashCoreVersion = verAttribute.InformationalVersion; } } - - /// - /// Used to save memory when shutting down - /// - /// - static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + else { - - if (programEventType == eProgramStatusEventType.Stopping) - { - Log.CloseAndFlush(); - - if (_saveTimer != null) - { - _saveTimer.Stop(); - _saveTimer = null; - } - LogMessage(LogEventLevel.Information, "Saving debug settings"); - SaveMemory(); - } + var version = assembly.GetName().Version; + PepperDashCoreVersion = version.ToString(); } + } - /// - /// Callback for console command - /// - /// - /// - /// SetDebugFromConsole method - /// - public static void SetDebugFromConsole(string levelString) + /// + /// Used to save memory when shutting down + /// + /// + static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + + if (programEventType == eProgramStatusEventType.Stopping) { - try + Log.CloseAndFlush(); + + if (_saveTimer != null) { - if (levelString.Trim() == "?") - { - CrestronConsole.ConsoleCommandResponse( - "Used to set the minimum level of debug messages:\r\n" + - "Usage: appdebug:P [sink] [level]\r\n" + - " sink: console (default), errorlog, file, all\r\n" + - " all: sets all sinks to the specified level\r\n" + - " level: 0-5 or LogEventLevel name\r\n" + - $"{_logLevels[0]} = 0\r\n" + - $"{_logLevels[1]} = 1\r\n" + - $"{_logLevels[2]} = 2\r\n" + - $"{_logLevels[3]} = 3\r\n" + - $"{_logLevels[4]} = 4\r\n" + - $"{_logLevels[5]} = 5"); - return; - } - - if (string.IsNullOrEmpty(levelString.Trim())) - { - CrestronConsole.ConsoleCommandResponse("Console log level = {0}\r\n", _consoleLogLevelSwitch.MinimumLevel); - CrestronConsole.ConsoleCommandResponse("File log level = {0}\r\n", _fileLogLevelSwitch.MinimumLevel); - CrestronConsole.ConsoleCommandResponse("Error log level = {0}\r\n", _errorLogLevelSwitch.MinimumLevel); - return; - } - - // Parse tokens: first token is sink (defaults to console), second token is level - var tokens = levelString.Trim().Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); - - string sinkName; - string levelToken; - - if (tokens.Length == 1) - { - // Single token - assume it's a level for console sink - sinkName = "console"; - levelToken = tokens[0]; - } - else if (tokens.Length == 2) - { - // Two tokens - first is sink, second is level - sinkName = tokens[0].ToLowerInvariant(); - levelToken = tokens[1]; - } - else - { - CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [sink] [level]"); - return; - } - - // Parse the level using the same logic as before - LogEventLevel level; - - if (int.TryParse(levelToken, out var levelInt)) - { - if (levelInt < 0 || levelInt > 5) - { - CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelToken} to valid log level. If using a number, value must be between 0-5"); - return; - } - - if (!_logLevels.TryGetValue((uint)levelInt, out level)) - { - level = LogEventLevel.Information; - CrestronConsole.ConsoleCommandResponse($"{levelInt} not valid. Setting level to {level}"); - } - } - else if (Enum.TryParse(levelToken, true, out level)) - { - // Successfully parsed as LogEventLevel enum - } - else - { - CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelToken} to valid log level"); - return; - } - - // Set the level for the specified sink - switch (sinkName) - { - case "console": - SetDebugLevel(level); - break; - case "errorlog": - SetErrorLogMinimumDebugLevel(level); - break; - case "file": - SetFileMinimumDebugLevel(level); - break; - case "all": - SetDebugLevel(level); - SetErrorLogMinimumDebugLevel(level); - SetFileMinimumDebugLevel(level); - break; - default: - CrestronConsole.ConsoleCommandResponse($"Error: Unknown sink '{sinkName}'. Valid sinks: console, errorlog, file"); - break; - } - } - catch - { - CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [sink] [level]"); + _saveTimer.Stop(); + _saveTimer = null; } + LogMessage(LogEventLevel.Information, "Saving debug settings"); + SaveMemory(); } + } - /// - /// Sets the debug level - /// - /// Valid values 0-5 - /// - /// SetDebugLevel method - /// - public static void SetDebugLevel(uint level) + /// + /// Callback for console command + /// + /// + public static void SetDebugFromConsole(string levelString) + { + try { - if (!_logLevels.TryGetValue(level, out var logLevel)) + if (levelString.Trim() == "?") { - logLevel = LogEventLevel.Information; - - CrestronConsole.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}"); - - SetDebugLevel(logLevel); + CrestronConsole.ConsoleCommandResponse( + $@"Used to set the minimum level of debug messages to be printed to the console: +{_logLevels[0]} = 0 +{_logLevels[1]} = 1 +{_logLevels[2]} = 2 +{_logLevels[3]} = 3 +{_logLevels[4]} = 4 +{_logLevels[5]} = 5"); + return; } + if (string.IsNullOrEmpty(levelString.Trim())) + { + CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", _consoleLoggingLevelSwitch.MinimumLevel); + return; + } + + if(int.TryParse(levelString, out var levelInt)) + { + if(levelInt < 0 || levelInt > 5) + { + CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5"); + return; + } + SetDebugLevel((uint) levelInt); + return; + } + + if(Enum.TryParse(levelString, out var levelEnum)) + { + SetDebugLevel(levelEnum); + return; + } + + CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); + } + catch + { + CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); + } + } + + /// + /// Sets the debug level + /// + /// Valid values 0-5 + public static void SetDebugLevel(uint level) + { + if(!_logLevels.TryGetValue(level, out var logLevel)) + { + logLevel = LogEventLevel.Information; + + CrestronConsole.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}"); + SetDebugLevel(logLevel); } - /// - /// SetDebugLevel method - /// - public static void SetDebugLevel(LogEventLevel level) + SetDebugLevel(logLevel); + } + + public static void SetDebugLevel(LogEventLevel level) + { + _consoleLoggingLevelSwitch.MinimumLevel = level; + + CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}\r\n", + InitialParametersClass.ApplicationNumber, _consoleLoggingLevelSwitch.MinimumLevel); + + CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int) level}"); + + var err = CrestronDataStoreStatic.SetLocalIntValue(LevelStoreKey, (int) level); + + CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}"); + + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + CrestronConsole.PrintLine($"Error saving console debug level setting: {err}"); + } + + public static void SetWebSocketMinimumDebugLevel(LogEventLevel level) + { + _websocketLoggingLevelSwitch.MinimumLevel = level; + + var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint) level); + + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {erro}", err); + + LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); + } + + public static void SetErrorLogMinimumDebugLevel(LogEventLevel level) + { + _errorLogLevelSwitch.MinimumLevel = level; + + var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); + + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting: {error}", err); + + LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); + } + + public static void SetFileMinimumDebugLevel(LogEventLevel level) + { + _errorLogLevelSwitch.MinimumLevel = level; + + var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); + + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + LogMessage(LogEventLevel.Information, "Error saving File debug level setting: {error}", err); + + LogMessage(LogEventLevel.Information, "File debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); + } + + /// + /// Callback for console command + /// + /// + public static void SetDoNotLoadOnNextBootFromConsole(string stateString) + { + try { - _consoleLogLevelSwitch.MinimumLevel = level; - - CrestronConsole.ConsoleCommandResponse("[Application {0}] Debug level set to {1}\r\n", - InitialParametersClass.ApplicationNumber, _consoleLogLevelSwitch.MinimumLevel); - - CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); - - var err = CrestronDataStoreStatic.SetLocalIntValue(LevelStoreKey, (int)level); - - CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}"); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - CrestronConsole.PrintLine($"Error saving console debug level setting: {err}"); - } - - /// - /// SetWebSocketMinimumDebugLevel method - /// - public static void SetWebSocketMinimumDebugLevel(LogEventLevel level) - { - _websocketLogLevelSwitch.MinimumLevel = level; - - var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {erro}", err); - - LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", _websocketLogLevelSwitch.MinimumLevel); - } - - /// - /// SetErrorLogMinimumDebugLevel method - /// - public static void SetErrorLogMinimumDebugLevel(LogEventLevel level) - { - _errorLogLevelSwitch.MinimumLevel = level; - - CrestronConsole.ConsoleCommandResponse("[Application {0}] Error log level set to {1}\r\n", - InitialParametersClass.ApplicationNumber, _errorLogLevelSwitch.MinimumLevel); - - CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); - - var err = CrestronDataStoreStatic.SetLocalIntValue(ErrorLogLevelStoreKey, (int)level); - - CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}"); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - CrestronConsole.PrintLine($"Error saving error log debug level setting: {err}"); - } - - /// - /// SetFileMinimumDebugLevel method - /// - public static void SetFileMinimumDebugLevel(LogEventLevel level) - { - _fileLogLevelSwitch.MinimumLevel = level; - - CrestronConsole.ConsoleCommandResponse("[Application {0}] File log level set to {1}\r\n", - InitialParametersClass.ApplicationNumber, _fileLogLevelSwitch.MinimumLevel); - - CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); - - var err = CrestronDataStoreStatic.SetLocalIntValue(FileLevelStoreKey, (int)level); - - CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}"); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - CrestronConsole.PrintLine($"Error saving file debug level setting: {err}"); - } - - /// - /// Callback for console command - /// - /// - /// - /// SetDoNotLoadOnNextBootFromConsole method - /// - public static void SetDoNotLoadOnNextBootFromConsole(string stateString) - { - try + if (string.IsNullOrEmpty(stateString.Trim())) { - if (string.IsNullOrEmpty(stateString.Trim())) - { - CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadConfigOnNextBoot); - return; - } - - SetDoNotLoadConfigOnNextBoot(bool.Parse(stateString)); - } - catch - { - CrestronConsole.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]"); - } - } - - /// - /// Callback for console command - /// - /// - /// - /// SetDebugFilterFromConsole method - /// - public static void SetDebugFilterFromConsole(string items) - { - var str = items.Trim(); - if (str == "?") - { - CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " + - "+all: at beginning puts filter into 'default include' mode\r" + - " All keys that follow will be excluded from output.\r" + - "-all: at beginning puts filter into 'default exclude all' mode.\r" + - " All keys that follow will be the only keys that are shown\r" + - "+nokey: Enables messages with no key (default)\r" + - "-nokey: Disables messages with no key.\r" + - "(nokey settings are independent of all other settings)"); + CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadConfigOnNextBoot); return; } - var keys = Regex.Split(str, @"\s*"); - foreach (var keyToken in keys) - { - var lkey = keyToken.ToLower(); - if (lkey == "+all") - { - IncludedExcludedKeys.Clear(); - _excludeAllMode = false; - } - else if (lkey == "-all") - { - IncludedExcludedKeys.Clear(); - _excludeAllMode = true; - } - //else if (lkey == "+nokey") - //{ - // ExcludeNoKeyMessages = false; - //} - //else if (lkey == "-nokey") - //{ - // ExcludeNoKeyMessages = true; - //} - else - { - string key; - if (lkey.StartsWith("-")) - { - key = lkey.Substring(1); - // if in exclude all mode, we need to remove this from the inclusions - if (_excludeAllMode) - { - if (IncludedExcludedKeys.ContainsKey(key)) - IncludedExcludedKeys.Remove(key); - } - // otherwise include all mode, add to the exclusions - else - { - IncludedExcludedKeys[key] = new object(); - } - } - else if (lkey.StartsWith("+")) - { - key = lkey.Substring(1); - // if in exclude all mode, we need to add this as inclusion - if (_excludeAllMode) - { + + SetDoNotLoadConfigOnNextBoot(bool.Parse(stateString)); + } + catch + { + CrestronConsole.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]"); + } + } + + /// + /// Callback for console command + /// + /// + public static void SetDebugFilterFromConsole(string items) + { + var str = items.Trim(); + if (str == "?") + { + CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " + + "+all: at beginning puts filter into 'default include' mode\r" + + " All keys that follow will be excluded from output.\r" + + "-all: at beginning puts filter into 'default exclude all' mode.\r" + + " All keys that follow will be the only keys that are shown\r" + + "+nokey: Enables messages with no key (default)\r" + + "-nokey: Disables messages with no key.\r" + + "(nokey settings are independent of all other settings)"); + return; + } + var keys = Regex.Split(str, @"\s*"); + foreach (var keyToken in keys) + { + var lkey = keyToken.ToLower(); + if (lkey == "+all") + { + IncludedExcludedKeys.Clear(); + _excludeAllMode = false; + } + else if (lkey == "-all") + { + IncludedExcludedKeys.Clear(); + _excludeAllMode = true; + } + //else if (lkey == "+nokey") + //{ + // ExcludeNoKeyMessages = false; + //} + //else if (lkey == "-nokey") + //{ + // ExcludeNoKeyMessages = true; + //} + else + { + string key; + if (lkey.StartsWith("-")) + { + key = lkey.Substring(1); + // if in exclude all mode, we need to remove this from the inclusions + if (_excludeAllMode) + { + if (IncludedExcludedKeys.ContainsKey(key)) + IncludedExcludedKeys.Remove(key); + } + // otherwise include all mode, add to the exclusions + else + { + IncludedExcludedKeys[key] = new object(); + } + } + else if (lkey.StartsWith("+")) + { + key = lkey.Substring(1); + // if in exclude all mode, we need to add this as inclusion + if (_excludeAllMode) + { IncludedExcludedKeys[key] = new object(); } @@ -664,579 +546,488 @@ namespace PepperDash.Core; - /// - /// sets the settings for a device or creates a new entry - /// - /// - /// - /// - public static void SetDeviceDebugSettings(string deviceKey, object settings) + /// + /// sets the settings for a device or creates a new entry + /// + /// + /// + /// + public static void SetDeviceDebugSettings(string deviceKey, object settings) + { + _contexts.SetDebugSettingsForKey(deviceKey, settings); + SaveMemoryOnTimeout(); + } + + /// + /// Gets the device settings for a device by key or returns null + /// + /// + /// + public static object GetDeviceDebugSettingsForKey(string deviceKey) + { + return _contexts.GetDebugSettingsForKey(deviceKey); + } + + /// + /// Sets the flag to prevent application starting on next boot + /// + /// + public static void SetDoNotLoadConfigOnNextBoot(bool state) + { + DoNotLoadConfigOnNextBoot = state; + + var err = CrestronDataStoreStatic.SetLocalBoolValue(DoNotLoadOnNextBootKey, state); + + if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + LogError("Error saving console debug level setting: {err}", err); + + LogInformation("Do Not Load Config on Next Boot set to {state}", DoNotLoadConfigOnNextBoot); + } + + /// + /// + /// + public static void ShowDebugLog(string s) + { + var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all"); + foreach (var l in loglist) + CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); + } + + /// + /// Log an Exception as an Error + /// + /// Exception to log + /// Message template + /// Optional IKeyed device. If provided, the Key of the device will be added to the log message + /// Args to put into message template + public static void LogMessage(Exception ex, string message, IKeyed device = null, params object[] args) + { + using (LogContext.PushProperty("Key", device?.Key)) { - _contexts.SetDebugSettingsForKey(deviceKey, settings); - SaveMemoryOnTimeout(); + _logger.Error(ex, message, args); } + } - /// - /// Gets the device settings for a device by key or returns null - /// - /// - /// - /// - /// GetDeviceDebugSettingsForKey method - /// - public static object GetDeviceDebugSettingsForKey(string deviceKey) - { - return _contexts.GetDebugSettingsForKey(deviceKey); - } - - /// - /// Sets the flag to prevent application starting on next boot - /// - /// - public static void SetDoNotLoadConfigOnNextBoot(bool state) - { - DoNotLoadConfigOnNextBoot = state; - _contexts.GetOrCreateItem("DEFAULT").DoNotLoadOnNextBoot = state; - SaveMemoryOnTimeout(); - - CrestronConsole.ConsoleCommandResponse("[Application {0}], Do Not Load Config on Next Boot set to {1}", - InitialParametersClass.ApplicationNumber, DoNotLoadConfigOnNextBoot); - } - - /// - /// ShowDebugLog method - /// - public static void ShowDebugLog(string s) - { - var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all"); - foreach (var l in loglist) - CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); - } - - /// - /// Log an Exception as an Error - /// - /// Exception to log - /// Message template - /// Optional IKeyed device. If provided, the Key of the device will be added to the log message - /// Args to put into message template - /// - /// LogMessage method - /// - public static void LogMessage(Exception ex, string message, IKeyed device = null, params object[] args) - { - using (LogContext.PushProperty("Key", device?.Key)) - { - _logger.Error(ex, message, args); - } - } - - /// - /// Log a message - /// - /// Level to log at - /// Message template - /// Optional IKeyed device. If provided, the Key of the device will be added to the log message - /// Args to put into message template - public static void LogMessage(LogEventLevel level, string message, IKeyed device = null, params object[] args) - { - using (LogContext.PushProperty("Key", device?.Key)) - { - _logger.Write(level, message, args); - } - } - - /// - /// Logs a message at the specified log level. - /// - /// Level to log at - /// Message template - /// Args to put into message template - public static void LogMessage(LogEventLevel level, string message, params object[] args) + /// + /// Log a message + /// + /// Level to log at + /// Message template + /// Optional IKeyed device. If provided, the Key of the device will be added to the log message + /// Args to put into message template + public static void LogMessage(LogEventLevel level, string message, IKeyed device = null, params object[] args) + { + using (LogContext.PushProperty("Key", device?.Key)) { _logger.Write(level, message, args); } + } - /// - /// LogMessage method - /// - public static void LogMessage(LogEventLevel level, Exception ex, string message, params object[] args) + public static void LogMessage(LogEventLevel level, string message, params object[] args) + { + _logger.Write(level, message, args); + } + + public static void LogMessage(LogEventLevel level, Exception ex, string message, params object[] args) + { + _logger.Write(level, ex, message, args); + } + + public static void LogMessage(LogEventLevel level, IKeyed keyed, string message, params object[] args) + { + LogMessage(level, message, keyed, args); + } + + public static void LogMessage(LogEventLevel level, Exception ex, IKeyed device, string message, params object[] args) + { + using (LogContext.PushProperty("Key", device?.Key)) { _logger.Write(level, ex, message, args); } + } - /// - /// LogMessage method - /// - public static void LogMessage(LogEventLevel level, IKeyed keyed, string message, params object[] args) - { - LogMessage(level, message, keyed, args); - } - - /// - /// LogMessage method - /// - public static void LogMessage(LogEventLevel level, Exception ex, IKeyed device, string message, params object[] args) - { - using (LogContext.PushProperty("Key", device?.Key)) - { - _logger.Write(level, ex, message, args); - } - } - - #region Explicit methods for logging levels - /// - /// LogVerbose method - /// - public static void LogVerbose(IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Verbose, message, args); - } - } - - /// - /// LogVerbose method - /// - public static void LogVerbose(Exception ex, IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Verbose, ex, message, args); - } - } - - /// - /// LogVerbose method - /// - public static void LogVerbose(string message, params object[] args) + #region Explicit methods for logging levels + public static void LogVerbose(IKeyed keyed, string message, params object[] args) + { + using(LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Verbose, message, args); } + } - /// - /// LogVerbose method - /// - public static void LogVerbose(Exception ex, string message, params object[] args) + public static void LogVerbose(Exception ex, IKeyed keyed, string message, params object[] args) + { + using(LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Verbose, ex, message, args); } + } - /// - /// LogDebug method - /// - public static void LogDebug(IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Debug, message, args); - } - } + public static void LogVerbose(string message, params object[] args) + { + _logger.Write(LogEventLevel.Verbose, message, args); + } - /// - /// LogDebug method - /// - public static void LogDebug(Exception ex, IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Debug, ex, message, args); - } - } + public static void LogVerbose(Exception ex, string message, params object[] args) + { + _logger.Write(LogEventLevel.Verbose, ex, null, message, args); + } - /// - /// LogDebug method - /// - public static void LogDebug(string message, params object[] args) + public static void LogDebug(IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Debug, message, args); } + } - /// - /// LogDebug method - /// - public static void LogDebug(Exception ex, string message, params object[] args) + public static void LogDebug(Exception ex, IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { - _logger.Write(LogEventLevel.Debug, ex, null, message, args); + _logger.Write(LogEventLevel.Debug, ex, message, args); } + } - /// - /// LogInformation method - /// - public static void LogInformation(IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Information, message, args); - } - } + public static void LogDebug(string message, params object[] args) + { + _logger.Write(LogEventLevel.Debug, message, args); + } - /// - /// LogInformation method - /// - public static void LogInformation(Exception ex, IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Information, ex, message, args); - } - } + public static void LogDebug(Exception ex, string message, params object[] args) + { + _logger.Write(LogEventLevel.Debug, ex, null, message, args); + } - /// - /// LogInformation method - /// - public static void LogInformation(string message, params object[] args) + public static void LogInformation(IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Information, message, args); } + } - /// - /// LogInformation method - /// - public static void LogInformation(Exception ex, string message, params object[] args) + public static void LogInformation(Exception ex, IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Information, ex, message, args); } + } - /// - /// LogWarning method - /// - public static void LogWarning(IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Warning, message, args); - } - } + public static void LogInformation(string message, params object[] args) + { + _logger.Write(LogEventLevel.Information, message, args); + } - /// - /// LogWarning method - /// - public static void LogWarning(Exception ex, IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Warning, ex, message, args); - } - } + public static void LogInformation(Exception ex, string message, params object[] args) + { + _logger.Write(LogEventLevel.Information, ex, null, message, args); + } - /// - /// LogWarning method - /// - public static void LogWarning(string message, params object[] args) + public static void LogWarning(IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Warning, message, args); } + } - /// - /// LogWarning method - /// - public static void LogWarning(Exception ex, string message, params object[] args) + public static void LogWarning(Exception ex, IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Warning, ex, message, args); } + } - /// - /// LogError method - /// - public static void LogError(IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Error, message, args); - } - } + public static void LogWarning(string message, params object[] args) + { + _logger.Write(LogEventLevel.Warning, message, args); + } - /// - /// LogError method - /// - public static void LogError(Exception ex, IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Error, ex, message, args); - } - } + public static void LogWarning(Exception ex, string message, params object[] args) + { + _logger.Write(LogEventLevel.Warning, ex, null, message, args); + } - /// - /// LogError method - /// - public static void LogError(string message, params object[] args) + public static void LogError(IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Error, message, args); } + } - /// - /// LogError method - /// - public static void LogError(Exception ex, string message, params object[] args) + public static void LogError(Exception ex, IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Error, ex, message, args); } + } - /// - /// LogFatal method - /// - public static void LogFatal(IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Fatal, message, args); - } - } + public static void LogError(string message, params object[] args) + { + _logger.Write(LogEventLevel.Error, message, args); + } - /// - /// LogFatal method - /// - public static void LogFatal(Exception ex, IKeyed keyed, string message, params object[] args) - { - using (LogContext.PushProperty("Key", keyed?.Key)) - { - _logger.Write(LogEventLevel.Fatal, ex, message, args); - } - } + public static void LogError(Exception ex, string message, params object[] args) + { + _logger.Write(LogEventLevel.Error, ex, null, message, args); + } - /// - /// LogFatal method - /// - public static void LogFatal(string message, params object[] args) + public static void LogFatal(IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Fatal, message, args); } + } - /// - /// LogFatal method - /// - public static void LogFatal(Exception ex, string message, params object[] args) + public static void LogFatal(Exception ex, IKeyed keyed, string message, params object[] args) + { + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Fatal, ex, message, args); } + } - #endregion + public static void LogFatal(string message, params object[] args) + { + _logger.Write(LogEventLevel.Fatal, message, args); + } + + public static void LogFatal(Exception ex, string message, params object[] args) + { + _logger.Write(LogEventLevel.Fatal, ex, null, message, args); + } + + #endregion - private static void LogMessage(uint level, string format, params object[] items) - { - if (!_logLevels.ContainsKey(level)) return; + private static void LogMessage(uint level, string format, params object[] items) + { + if (!_logLevels.ContainsKey(level)) return; - var logLevel = _logLevels[level]; + var logLevel = _logLevels[level]; + + LogMessage(logLevel, format, items); + } - LogMessage(logLevel, format, items); - } + private static void LogMessage(uint level, IKeyed keyed, string format, params object[] items) + { + if (!_logLevels.ContainsKey(level)) return; - private static void LogMessage(uint level, IKeyed keyed, string format, params object[] items) - { - if (!_logLevels.ContainsKey(level)) return; + var logLevel = _logLevels[level]; - var logLevel = _logLevels[level]; - - LogMessage(logLevel, keyed, format, items); - } + LogMessage(logLevel, keyed, format, items); + } - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. - /// Uses CrestronConsole.PrintLine. - /// - /// - /// Console format string - /// Object parameters - [Obsolete("Use LogMessage methods. Will be removed in 2.2.0 and later versions")] - public static void Console(uint level, string format, params object[] items) - { + /// + /// Prints message to console if current debug level is equal to or higher than the level of this message. + /// Uses CrestronConsole.PrintLine. + /// + /// + /// Console format string + /// Object parameters + [Obsolete("Use LogMessage methods. Will be removed in 2.2.0 and later versions")] + public static void Console(uint level, string format, params object[] items) + { - LogMessage(level, format, items); + LogMessage(level, format, items); - //if (IsRunningOnAppliance) - //{ - // CrestronConsole.PrintLine("[{0}]App {1} Lvl {2}:{3}", DateTime.Now.ToString("HH:mm:ss.fff"), - // InitialParametersClass.ApplicationNumber, - // level, - // string.Format(format, items)); - //} - } + //if (IsRunningOnAppliance) + //{ + // CrestronConsole.PrintLine("[{0}]App {1} Lvl {2}:{3}", DateTime.Now.ToString("HH:mm:ss.fff"), + // InitialParametersClass.ApplicationNumber, + // level, + // string.Format(format, items)); + //} + } - /// + /// /// Logs to Console when at-level, and all messages to error log, including device key - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void Console(uint level, IKeyed dev, string format, params object[] items) - { - LogMessage(level, dev, format, items); + /// + [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] + public static void Console(uint level, IKeyed dev, string format, params object[] items) + { + LogMessage(level, dev, format, items); - //if (Level >= level) - // Console(level, "[{0}] {1}", dev.Key, message); + //if (Level >= level) + // Console(level, "[{0}] {1}", dev.Key, message); + } + + /// + /// Prints message to console if current debug level is equal to or higher than the level of this message. Always sends message to Error Log. + /// Uses CrestronConsole.PrintLine. + /// + [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] + public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, + string format, params object[] items) + { + LogMessage(level, dev, format, items); + } + + /// + /// 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")] + public static void Console(uint level, ErrorLogLevel errorLogLevel, + string format, params object[] items) + { + LogMessage(level, format, items); + } + + /// + /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at + /// or above the level provided, then the output will be written to both console and the log. Otherwise + /// it will only be written to the log. + /// + [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] + public static void ConsoleWithLog(uint level, string format, params object[] items) + { + LogMessage(level, format, items); + + // var str = string.Format(format, items); + //if (Level >= level) + // CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); + // CrestronLogger.WriteToLog(str, level); + } + + /// + /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at + /// or above the level provided, then the output will be written to both console and the log. Otherwise + /// it will only be written to the log. + /// + [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] + public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items) + { + LogMessage(level, dev, format, items); + + // var str = string.Format(format, items); + // CrestronLogger.WriteToLog(string.Format("[{0}] {1}", dev.Key, str), level); + } + + /// + /// Prints to log and error log + /// + /// + /// + [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] + public static void LogError(ErrorLogLevel errorLogLevel, string str) + { + switch (errorLogLevel) + { + case ErrorLogLevel.Error: + LogMessage(LogEventLevel.Error, str); + break; + case ErrorLogLevel.Warning: + LogMessage(LogEventLevel.Warning, str); + break; + case ErrorLogLevel.Notice: + LogMessage(LogEventLevel.Information, str); + break; } + } - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. Always sends message to Error Log. - /// Uses CrestronConsole.PrintLine. - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - LogMessage(level, dev, format, items); - } - - /// - /// 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")] - public static void Console(uint level, ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - LogMessage(level, format, items); - } - - /// - /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at - /// or above the level provided, then the output will be written to both console and the log. Otherwise - /// it will only be written to the log. - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void ConsoleWithLog(uint level, string format, params object[] items) - { - LogMessage(level, format, items); - - // var str = string.Format(format, items); - //if (Level >= level) - // CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); - // CrestronLogger.WriteToLog(str, level); - } - - /// - /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at - /// or above the level provided, then the output will be written to both console and the log. Otherwise - /// it will only be written to the log. - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items) - { - LogMessage(level, dev, format, items); - - // var str = string.Format(format, items); - // CrestronLogger.WriteToLog(string.Format("[{0}] {1}", dev.Key, str), level); - } - - /// - /// Prints to log and error log - /// - /// - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void LogError(ErrorLogLevel errorLogLevel, string str) - { - switch (errorLogLevel) + /// + /// Writes the memory object after timeout + /// + static void SaveMemoryOnTimeout() + { + Console(0, "Saving debug settings"); + if (_saveTimer == null) + _saveTimer = new CTimer(o => { - case ErrorLogLevel.Error: - LogMessage(LogEventLevel.Error, str); - break; - case ErrorLogLevel.Warning: - LogMessage(LogEventLevel.Warning, str); - break; - case ErrorLogLevel.Notice: - LogMessage(LogEventLevel.Information, str); - break; - } - } + _saveTimer = null; + SaveMemory(); + }, SaveTimeoutMs); + else + _saveTimer.Reset(SaveTimeoutMs); + } - /// - /// Writes the memory object after timeout - /// - static void SaveMemoryOnTimeout() + /// + /// Writes the memory - use SaveMemoryOnTimeout + /// + static void SaveMemory() + { + //var dir = @"\NVRAM\debug"; + //if (!Directory.Exists(dir)) + // Directory.Create(dir); + + var fileName = GetMemoryFileName(); + + LogMessage(LogEventLevel.Information, "Loading debug settings file from {fileName}", fileName); + + using (var sw = new StreamWriter(fileName)) { - Console(0, "Saving debug settings"); - if (_saveTimer == null) - _saveTimer = new CTimer(o => + var json = JsonConvert.SerializeObject(_contexts); + sw.Write(json); + sw.Flush(); + } + } + + /// + /// + /// + static void LoadMemory() + { + var file = GetMemoryFileName(); + if (File.Exists(file)) + { + using (var sr = new StreamReader(file)) + { + var json = sr.ReadToEnd(); + _contexts = JsonConvert.DeserializeObject(json); + + if (_contexts != null) { - _saveTimer = null; - SaveMemory(); - }, SaveTimeoutMs); - else - _saveTimer.Reset(SaveTimeoutMs); - } - - /// - /// Writes the memory - use SaveMemoryOnTimeout - /// - static void SaveMemory() - { - //var dir = @"\NVRAM\debug"; - //if (!Directory.Exists(dir)) - // Directory.Create(dir); - - var fileName = GetMemoryFileName(); - - LogMessage(LogEventLevel.Information, "Loading debug settings file from {fileName}", fileName); - - using (var sw = new StreamWriter(fileName)) - { - var json = JsonConvert.SerializeObject(_contexts); - sw.Write(json); - sw.Flush(); - } - } - - /// - /// - /// - static void LoadMemory() - { - var file = GetMemoryFileName(); - if (File.Exists(file)) - { - using (var sr = new StreamReader(file)) - { - var json = sr.ReadToEnd(); - _contexts = JsonConvert.DeserializeObject(json); - - if (_contexts != null) - { - LogMessage(LogEventLevel.Debug, "Debug memory restored from file"); - return; - } + LogMessage(LogEventLevel.Debug, "Debug memory restored from file"); + return; } } - - _contexts = new DebugContextCollection(); } - /// - /// Helper to get the file path for this app's debug memory - /// - static string GetMemoryFileName() - { - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) - { - // CheckForMigration(); - return string.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); - } + _contexts = new DebugContextCollection(); + } - return string.Format("{0}{1}user{1}debugSettings{1}{2}.json", Directory.GetApplicationRootDirectory(), Path.DirectorySeparatorChar, InitialParametersClass.RoomId); + /// + /// Helper to get the file path for this app's debug memory + /// + static string GetMemoryFileName() + { + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) + { + // CheckForMigration(); + return string.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); } + return string.Format("{0}{1}user{1}debugSettings{1}{2}.json",Directory.GetApplicationRootDirectory(), Path.DirectorySeparatorChar, InitialParametersClass.RoomId); + } + + /// + /// Error level to for message to be logged at + /// + public enum ErrorLogLevel + { /// - /// Enumeration of ErrorLogLevel values + /// Error /// - public enum ErrorLogLevel - { - /// - /// Error - /// - Error, - /// - /// Warning - /// - Warning, - /// - /// Notice - /// - Notice, - /// - /// None - /// - None, + Error, + /// + /// Warning + /// + Warning, + /// + /// Notice + /// + Notice, + /// + /// None + /// + None, } } \ No newline at end of file diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index d62b3c8b..f6e5a2f4 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -17,326 +17,320 @@ using WebSocketSharp.Server; using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; using WebSocketSharp.Net; -namespace PepperDash.Core +namespace PepperDash.Core; + +/// +/// Provides a WebSocket-based logging sink for debugging purposes, allowing log events to be broadcast to connected +/// WebSocket clients. +/// +/// This class implements the interface and is designed to send +/// formatted log events to WebSocket clients connected to a secure WebSocket server. The server is hosted locally +/// and uses a self-signed certificate for SSL/TLS encryption. +public class DebugWebsocketSink : ILogEventSink, IKeyed { + private HttpServer _httpsServer; + + private readonly string _path = "/debug/join/"; + private const string _certificateName = "selfCres"; + private const string _certificatePassword = "cres12345"; + /// - /// Provides a WebSocket-based logging sink for debugging purposes, allowing log events to be broadcast to connected - /// WebSocket clients. + /// Gets the port number on which the HTTPS server is currently running. /// - /// This class implements the interface and is designed to send - /// formatted log events to WebSocket clients connected to a secure WebSocket server. The server is hosted locally - /// and uses a self-signed certificate for SSL/TLS encryption. - public class DebugWebsocketSink : ILogEventSink, IKeyed + public int Port + { get + { + + if(_httpsServer == null) return 0; + return _httpsServer.Port; + } + } + + /// + /// Gets the WebSocket URL for the current server instance. + /// + /// The URL is dynamically constructed based on the server's current IP address, port, + /// and WebSocket path. + public string Url { - private WebSocketServer _wsServer; - - private readonly string _path = "/debug/join/"; - private const string _certificateName = "selfCres"; - private const string _certificatePassword = "cres12345"; - - /// - /// Gets the port number on which the HTTPS server is currently running. - /// - public int Port - { get - { - - if(_wsServer == null) return 0; - return _wsServer.Port; - } - } - - /// - /// Gets the WebSocket URL for the current server instance. - /// - /// The URL is dynamically constructed based on the server's current IP address, port, - /// and WebSocket path. - public string Url + get { - get + if (_httpsServer == null) return ""; + return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}"; + } + } + + /// + /// Gets a value indicating whether the HTTPS server is currently listening for incoming connections. + /// + public bool IsRunning { get => _httpsServer?.IsListening ?? false; } + + /// + public string Key => "DebugWebsocketSink"; + + private readonly ITextFormatter _textFormatter; + + /// + /// Initializes a new instance of the class with the specified text formatter. + /// + /// This constructor initializes the WebSocket sink and ensures that a certificate is + /// available for secure communication. If the required certificate does not exist, it will be created + /// automatically. Additionally, the sink is configured to stop the server when the program is + /// stopping. + /// The text formatter used to format log messages. If null, a default JSON formatter is used. + public DebugWebsocketSink(ITextFormatter formatProvider) + { + + _textFormatter = formatProvider ?? new JsonFormatter(); + + if (!File.Exists($"\\user\\{_certificateName}.pfx")) + CreateCert(); + + CrestronEnvironment.ProgramStatusEventHandler += type => + { + if (type == eProgramStatusEventType.Stopping) { - if (_wsServer == null) return ""; - return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_wsServer.Port}{_wsServer.WebSocketServices[_path].Path}"; + StopServer(); } + }; + } + + private static void CreateCert() + { + try + { + var utility = new BouncyCertificate(); + + var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); + var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0); + + CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress)); + + var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), [string.Format("{0}.{1}", hostName, domainName), ipAddress], [KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth]); + + //Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested + + var separator = Path.DirectorySeparatorChar; + + utility.CertificatePassword = _certificatePassword; + utility.WriteCertificate(certificate, @$"{separator}user{separator}", _certificateName); } - - /// - /// Gets a value indicating whether the WebSocket server is currently listening for incoming connections. - /// - public bool IsRunning { get => _wsServer?.IsListening ?? false; } - - /// - public string Key => "DebugWebsocketSink"; - - private readonly ITextFormatter _textFormatter; - - /// - /// Initializes a new instance of the class with the specified text formatter. - /// - /// This constructor initializes the WebSocket sink and ensures that a certificate is - /// available for secure communication. If the required certificate does not exist, it will be created - /// automatically. Additionally, the sink is configured to stop the server when the program is - /// stopping. - /// The text formatter used to format log messages. If null, a default JSON formatter is used. - public DebugWebsocketSink(ITextFormatter formatProvider) + catch (Exception ex) { + //Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); + CrestronConsole.PrintLine("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); + } + } - _textFormatter = formatProvider ?? new JsonFormatter(); + /// + /// Sends a log event to all connected WebSocket clients. + /// + /// The log event is formatted using the configured text formatter and then broadcasted + /// to all clients connected to the WebSocket server. If the WebSocket server is not initialized or not + /// listening, the method exits without performing any action. + /// The log event to be formatted and broadcasted. Cannot be null. + public void Emit(LogEvent logEvent) + { + if (_httpsServer == null || !_httpsServer.IsListening) return; - if (!File.Exists($"\\user\\{_certificateName}.pfx")) - CreateCert(); + var sw = new StringWriter(); + _textFormatter.Format(logEvent, sw); - CrestronEnvironment.ProgramStatusEventHandler += type => + _httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString()); + } + + /// + /// Starts the WebSocket server on the specified port and configures it with the appropriate certificate. + /// + /// This method initializes the WebSocket server and binds it to the specified port. It + /// also applies the server's certificate for secure communication. Ensure that the port is not already in use + /// and that the certificate file is accessible. + /// The port number on which the WebSocket server will listen. Must be a valid, non-negative port number. + public void StartServerAndSetPort(int port) + { + Debug.Console(0, "Starting Websocket Server on port: {0}", port); + + + Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword); + } + + private void Start(int port, string certPath = "", string certPassword = "") + { + try + { + _httpsServer = new HttpServer(port, true); + + if (!string.IsNullOrWhiteSpace(certPath)) { - if (type == eProgramStatusEventType.Stopping) - { - StopServer(); - } - }; - } + Debug.Console(0, "Assigning SSL Configuration"); - private static void CreateCert() - { - try - { - var utility = new BouncyCertificate(); - - var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); - var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); - var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0); - - CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress)); - - var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), [string.Format("{0}.{1}", hostName, domainName), ipAddress], [KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth]); - - //Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested - - utility.CertificatePassword = _certificatePassword; - utility.WriteCertificate(certificate, @"\user\", _certificateName); - } - catch (Exception ex) - { - //Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); - Debug.LogError("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); - } - } - - /// - /// Sends a log event to all connected WebSocket clients. - /// - /// The log event is formatted using the configured text formatter and then broadcasted - /// to all clients connected to the WebSocket server. If the WebSocket server is not initialized or not - /// listening, the method exits without performing any action. - /// The log event to be formatted and broadcasted. Cannot be null. - public void Emit(LogEvent logEvent) - { - if (_wsServer == null || !_wsServer.IsListening) return; - - var sw = new StringWriter(); - _textFormatter.Format(logEvent, sw); - - _wsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString()); - } - - /// - /// Starts the WebSocket server on the specified port and configures it with the appropriate certificate. - /// - /// This method initializes the WebSocket server and binds it to the specified port. It - /// also applies the server's certificate for secure communication. Ensure that the port is not already in use - /// and that the certificate file is accessible. - /// The port number on which the WebSocket server will listen. Must be a valid, non-negative port number. - public void StartServerAndSetPort(int port) - { - Debug.LogInformation("Starting Websocket Server on port: {0}", port); - - - Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword); - } - - private void Start(int port, string certPath = "", string certPassword = "") - { - try - { - ServerSslConfiguration sslConfig = null; - - if (!string.IsNullOrWhiteSpace(certPath)) - { - Debug.LogInformation("Assigning SSL Configuration"); - sslConfig = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword)) + _httpsServer.SslConfiguration.ServerCertificate = new X509Certificate2(certPath, certPassword); + _httpsServer.SslConfiguration.ClientCertificateRequired = false; + _httpsServer.SslConfiguration.CheckCertificateRevocation = false; + _httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12; + //this is just to test, you might want to actually validate + _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { - ClientCertificateRequired = false, - CheckCertificateRevocation = false, - EnabledSslProtocols = SslProtocols.Tls12, - //this is just to test, you might want to actually validate - ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => - { - Debug.LogInformation("HTTPS ClientCerticateValidation Callback triggered"); - return true; - } + Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered"); + return true; }; - } + } + Debug.Console(0, "Adding Debug Client Service"); + _httpsServer.AddWebSocketService(_path); + Debug.Console(0, "Assigning Log Info"); + _httpsServer.Log.Level = LogLevel.Trace; + _httpsServer.Log.Output = (d, s) => + { + uint level; - _wsServer = new WebSocketServer(port, true); - if (sslConfig != null) + switch(d.Level) { - _wsServer.SslConfiguration.ServerCertificate = sslConfig.ServerCertificate; + case WebSocketSharp.LogLevel.Fatal: + level = 3; + break; + case WebSocketSharp.LogLevel.Error: + level = 2; + break; + case WebSocketSharp.LogLevel.Warn: + level = 1; + break; + case WebSocketSharp.LogLevel.Info: + level = 0; + break; + case WebSocketSharp.LogLevel.Debug: + level = 4; + break; + case WebSocketSharp.LogLevel.Trace: + level = 5; + break; + default: + level = 4; + break; } - Debug.LogInformation("Adding Debug Client Service"); - _wsServer.AddWebSocketService(_path); - Debug.LogInformation("Assigning Log Info"); - _wsServer.Log.Level = LogLevel.Trace; - _wsServer.Log.Output = (d, s) => - { - uint level; + Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); + }; + Debug.Console(0, "Starting"); - switch(d.Level) - { - case WebSocketSharp.LogLevel.Fatal: - level = 3; - break; - case WebSocketSharp.LogLevel.Error: - level = 2; - break; - case WebSocketSharp.LogLevel.Warn: - level = 1; - break; - case WebSocketSharp.LogLevel.Info: - level = 0; - break; - case WebSocketSharp.LogLevel.Debug: - level = 4; - break; - case WebSocketSharp.LogLevel.Trace: - level = 5; - break; - default: - level = 4; - break; - } - - Debug.LogInformation("{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); - }; - Debug.LogInformation("Starting"); - - _wsServer.Start(); - Debug.LogInformation("Ready"); - } - catch (Exception ex) - { - Debug.LogError("WebSocket Failed to start {0}", ex.Message); - } + _httpsServer.Start(); + Debug.Console(0, "Ready"); } - - /// - /// Stops the WebSocket server if it is currently running. - /// - /// This method halts the WebSocket server and releases any associated resources. After - /// calling this method, the server will no longer accept or process incoming connections. - public void StopServer() + catch (Exception ex) { - Debug.LogInformation("Stopping Websocket Server"); - _wsServer?.Stop(); - - _wsServer = null; + Debug.Console(0, "WebSocket Failed to start {0}", ex.Message); } } /// - /// Configures the logger to write log events to a debug WebSocket sink. + /// Stops the WebSocket server if it is currently running. /// - /// This extension method allows you to direct log events to a WebSocket sink for debugging - /// purposes. - public static class DebugWebsocketSinkExtensions + /// This method halts the WebSocket server and releases any associated resources. After + /// calling this method, the server will no longer accept or process incoming connections. + public void StopServer() { - /// - /// Configures a logger to write log events to a debug WebSocket sink. - /// - /// This method adds a sink that writes log events to a WebSocket for debugging purposes. - /// It is typically used during development to stream log events in real-time. - /// The logger sink configuration to apply the WebSocket sink to. - /// An optional text formatter to format the log events. If not provided, a default formatter will be used. - /// A object that can be used to further configure the logger. - public static LoggerConfiguration DebugWebsocketSink( - this LoggerSinkConfiguration loggerConfiguration, - ITextFormatter formatProvider = null) - { - return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider)); - } - } + Debug.Console(0, "Stopping Websocket Server"); + _httpsServer?.Stop(); - /// - /// Represents a WebSocket client for debugging purposes, providing connection lifecycle management and message - /// handling functionality. - /// - /// The class extends to handle - /// WebSocket connections, including events for opening, closing, receiving messages, and errors. It tracks the - /// duration of the connection and logs relevant events for debugging. - public class DebugClient : WebSocketBehavior - { - private DateTime _connectionTime; - - /// - /// Gets the duration of time the WebSocket connection has been active. - /// - public TimeSpan ConnectedDuration - { - get - { - if (Context.WebSocket.IsAlive) - { - return DateTime.Now - _connectionTime; - } - else - { - return new TimeSpan(0); - } - } - } - - /// - /// Initializes a new instance of the class. - /// - /// This constructor creates a new instance and logs its creation. - public DebugClient() - { - Debug.LogInformation("DebugClient Created"); - } - - /// - protected override void OnOpen() - { - base.OnOpen(); - - var url = Context.WebSocket.Url; - Debug.LogInformation("New WebSocket Connection from: {0}", url); - - _connectionTime = DateTime.Now; - } - - /// - protected override void OnMessage(MessageEventArgs e) - { - base.OnMessage(e); - - Debug.LogInformation("WebSocket UiClient Message: {0}", e.Data); - } - - /// - protected override void OnClose(CloseEventArgs e) - { - base.OnClose(e); - - Debug.LogInformation("WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); - - } - - /// - protected override void OnError(WebSocketSharp.ErrorEventArgs e) - { - base.OnError(e); - - Debug.LogError("WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); - } + _httpsServer = null; + } +} + +/// +/// Configures the logger to write log events to a debug WebSocket sink. +/// +/// This extension method allows you to direct log events to a WebSocket sink for debugging +/// purposes. +public static class DebugWebsocketSinkExtensions +{ + /// + /// Configures a logger to write log events to a debug WebSocket sink. + /// + /// This method adds a sink that writes log events to a WebSocket for debugging purposes. + /// It is typically used during development to stream log events in real-time. + /// The logger sink configuration to apply the WebSocket sink to. + /// An optional text formatter to format the log events. If not provided, a default formatter will be used. + /// A object that can be used to further configure the logger. + public static LoggerConfiguration DebugWebsocketSink( + this LoggerSinkConfiguration loggerConfiguration, + ITextFormatter formatProvider = null) + { + return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider)); + } +} + +/// +/// Represents a WebSocket client for debugging purposes, providing connection lifecycle management and message +/// handling functionality. +/// +/// The class extends to handle +/// WebSocket connections, including events for opening, closing, receiving messages, and errors. It tracks the +/// duration of the connection and logs relevant events for debugging. +public class DebugClient : WebSocketBehavior +{ + private DateTime _connectionTime; + + /// + /// Gets the duration of time the WebSocket connection has been active. + /// + public TimeSpan ConnectedDuration + { + get + { + if (Context.WebSocket.IsAlive) + { + return DateTime.Now - _connectionTime; + } + else + { + return new TimeSpan(0); + } + } + } + + /// + /// Initializes a new instance of the class. + /// + /// This constructor creates a new instance and logs its + /// creation using the method with a debug level of 0. + public DebugClient() + { + Debug.Console(0, "DebugClient Created"); + } + + /// + protected override void OnOpen() + { + base.OnOpen(); + + var url = Context.WebSocket.Url; + Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); + + _connectionTime = DateTime.Now; + } + + /// + protected override void OnMessage(MessageEventArgs e) + { + base.OnMessage(e); + + Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data); + } + + /// + protected override void OnClose(CloseEventArgs e) + { + base.OnClose(e); + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); + + } + + /// + protected override void OnError(WebSocketSharp.ErrorEventArgs e) + { + base.OnError(e); + + Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); } } From 574e4dfb0f7f8db4c46dfba73ea8702e79f1e742 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 16:07:45 -0500 Subject: [PATCH 015/161] feat: modify factory loading Updating IDeviceFactory to resolve [FEATURE]-Refactor Plugin loading mechanism #1065. This change should be backwards-compatible with existing plugins that use the EssentialsPluginDeviceFactory class, as the interfaces are implemented by the various base classes. In addition, the correct assembly name is now printed when a type is loaded. --- .../Devices/DeviceManager.cs | 995 +++++++------- .../Factory/DeviceFactory.cs | 487 ++++--- .../ProcessorExtensionDeviceFactory.cs | 249 ++-- .../DeviceFactory.cs | 77 +- .../MobileControlFactory.cs | 85 +- src/PepperDash.Essentials/ControlSystem.cs | 1160 ++++++++--------- .../Factory/DeviceFactory.cs | 78 +- .../PepperDash.Essentials.csproj | 23 - 8 files changed, 1543 insertions(+), 1611 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index c199773a..915b10d0 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -8,527 +8,528 @@ using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public static class DeviceManager { + public static event EventHandler AllDevicesActivated; + public static event EventHandler AllDevicesRegistered; + public static event EventHandler AllDevicesInitialized; + + private static readonly CCriticalSection DeviceCriticalSection = new CCriticalSection(); + + //public static List Devices { get { return _Devices; } } + //static List _Devices = new List(); + + private static readonly Dictionary Devices = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// - /// Manages the devices in the system + /// Returns a copy of all the devices in a list /// - public static class DeviceManager + public static List AllDevices => [.. Devices.Values]; + + public static bool AddDeviceEnabled; + + /// + /// Initializes the control system by enabling device management and registering console commands. + /// + /// This method sets up the control system for device management by enabling the addition of + /// devices and registering a series of console commands for interacting with devices. These commands allow + /// operators to list device statuses, feedbacks, and managed devices, as well as perform actions such as + /// simulating communication, debugging device streams, and accessing device properties and methods. + /// The instance representing the control system to initialize. + public static void Initialize(CrestronControlSystem cs) { - /// - /// Raised when all devices have been activated - /// - public static event EventHandler AllDevicesActivated; + AddDeviceEnabled = true; - /// - /// Raised when all devices have been registered - /// - public static event EventHandler AllDevicesRegistered; + CrestronConsole.AddNewConsoleCommand(ListDeviceCommStatuses, "devcommstatus", "Lists the communication status of all devices", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListDeviceFeedbacks, "devfb", "Lists current feedbacks", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListDevices, "devlist", "Lists current managed devices", + 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(SimulateComReceiveOnDevice, "devsimreceive", + "Simulates incoming data on a com device", ConsoleAccessLevelEnum.AccessOperator); - /// - /// Raised when all devices have been initialized - /// - public static event EventHandler AllDevicesInitialized; + CrestronConsole.AddNewConsoleCommand(SetDeviceStreamDebugging, "setdevicestreamdebug", "set comm debug [deviceKey] [off/rx/tx/both] ([minutes])", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => DisableAllDeviceStreamDebugging(), "disableallstreamdebug", "disables stream debugging on all devices", ConsoleAccessLevelEnum.AccessOperator); + } - private static readonly CCriticalSection DeviceCriticalSection = new CCriticalSection(); - - private static readonly CEvent AllowAddDevicesCEvent = new CEvent(false, true); - - private static readonly Dictionary Devices = new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets or sets the AllDevices - /// - public static List AllDevices { get { return new List(Devices.Values); } } - - /// - /// Gets or sets the AddDeviceEnabled - /// - public static bool AddDeviceEnabled; - - /// - /// Initialize method - /// - public static void Initialize(CrestronControlSystem cs) + /// + /// Calls activate steps on all Device class items + /// + public static void ActivateAll() + { + try { - AddDeviceEnabled = true; - CrestronConsole.AddNewConsoleCommand(ListDeviceCommStatuses, "devcommstatus", "Lists the communication status of all devices", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(ListDeviceFeedbacks, "devfb", "Lists current feedbacks", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(ListDevices, "devlist", "Lists current managed devices", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(DeviceJsonApi.DoDeviceActionWithJson, "devjson", "", - 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); + OnAllDevicesRegistered(); - CrestronConsole.AddNewConsoleCommand(SetDeviceStreamDebugging, "setdevicestreamdebug", "set comm debug [deviceKey] [off/rx/tx/both] ([minutes])", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => DisableAllDeviceStreamDebugging(), "disableallstreamdebug", "disables stream debugging on all devices", ConsoleAccessLevelEnum.AccessOperator); - } - - /// - /// ActivateAll method - /// - public static void ActivateAll() - { - try - { - OnAllDevicesRegistered(); - - DeviceCriticalSection.Enter(); - AddDeviceEnabled = false; - // PreActivate all devices - Debug.LogMessage(LogEventLevel.Information, "****PreActivation starting...****"); - foreach (var d in Devices.Values) - { - try - { - if (d is Device) - (d as Device).PreActivate(); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} PreActivation failure: {0}", e.Message, d.Key); - Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); - } - } - Debug.LogMessage(LogEventLevel.Information, "****PreActivation complete****"); - Debug.LogMessage(LogEventLevel.Information, "****Activation starting...****"); - - // Activate all devices - foreach (var d in Devices.Values) - { - try - { - if (d is Device) - (d as Device).Activate(); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} Activation failure: {0}", e.Message, d.Key); - Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); - } - } - - Debug.LogMessage(LogEventLevel.Information, "****Activation complete****"); - Debug.LogMessage(LogEventLevel.Information, "****PostActivation starting...****"); - - // PostActivate all devices - foreach (var d in Devices.Values) - { - try - { - if (d is Device) - (d as Device).PostActivate(); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} PostActivation failure: {0}", e.Message, d.Key); - Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); - } - } - - Debug.LogMessage(LogEventLevel.Information, "****PostActivation complete****"); - - OnAllDevicesActivated(); - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - private static void DeviceManager_Initialized(object sender, EventArgs e) - { - var allInitialized = Devices.Values.OfType().All(d => d.IsInitialized); - - if (allInitialized) - { - Debug.LogMessage(LogEventLevel.Information, "****All Devices Initalized****"); - - OnAllDevicesInitialized(); - } - } - - private static void OnAllDevicesActivated() - { - var handler = AllDevicesActivated; - if (handler != null) - { - handler(null, new EventArgs()); - } - } - - private static void OnAllDevicesRegistered() - { - var handler = AllDevicesRegistered; - if (handler != null) - { - handler(null, new EventArgs()); - } - } - - private static void OnAllDevicesInitialized() - { - var handler = AllDevicesInitialized; - if (handler != null) - { - handler(null, new EventArgs()); - } - } - - /// - /// DeactivateAll method - /// - public static void DeactivateAll() - { - try - { - DeviceCriticalSection.Enter(); - foreach (var d in Devices.Values.OfType()) - { - d.Deactivate(); - } - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - private static void ListDevices(string s) - { - CrestronConsole.ConsoleCommandResponse($"{Devices.Count} Devices registered with Device Manager:\r\n"); - - var sorted = Devices.Values.ToList(); - sorted.Sort((a, b) => a.Key.CompareTo(b.Key)); - - foreach (var d in sorted) - { - var name = d is IKeyName ? (d as IKeyName).Name : "---"; - CrestronConsole.ConsoleCommandResponse($" [{d.Key}] {name}\r\n"); - } - } - - private static void ListDeviceFeedbacks(string devKey) - { - var dev = GetDeviceForKey(devKey); - if (dev == null) - { - CrestronConsole.ConsoleCommandResponse($"Device '{devKey}' not found\r\n"); - return; - } - if (!(dev is IHasFeedback statusDev)) - { - CrestronConsole.ConsoleCommandResponse($"Device '{devKey}' does not have visible feedbacks\r\n"); - return; - } - statusDev.DumpFeedbacksToConsole(true); - } - - private static void ListDeviceCommStatuses(string input) - { - - foreach (var dev in Devices.Values.OfType()) - { - CrestronConsole.ConsoleCommandResponse($"{dev}: {dev.CommunicationMonitor.Status}\r\n"); - } - } - - /// - /// AddDevice method - /// - public static void AddDevice(IKeyed newDev) - { - try - { - if (!DeviceCriticalSection.TryEnter()) - { - Debug.LogMessage(LogEventLevel.Information, "Currently unable to add devices to Device Manager. Please try again"); - return; - } - // Check for device with same key - //var existingDevice = _Devices.FirstOrDefault(d => d.Key.Equals(newDev.Key, StringComparison.OrdinalIgnoreCase)); - ////// If it exists, remove or warn?? - //if (existingDevice != null) - - if (!AddDeviceEnabled) - { - Debug.LogMessage(LogEventLevel.Information, "All devices have been activated. Adding new devices is not allowed."); - return; - } - - if (Devices.ContainsKey(newDev.Key)) - { - Debug.LogMessage(LogEventLevel.Information, newDev, "WARNING: A device with this key already exists. Not added to manager"); - return; - } - Devices.Add(newDev.Key, newDev); - //if (!(_Devices.Contains(newDev))) - // _Devices.Add(newDev); - - if (newDev is EssentialsDevice essentialsDev) - essentialsDev.Initialized += DeviceManager_Initialized; - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - /// - /// AddDevice method - /// - public static void AddDevice(IEnumerable devicesToAdd) - { - try - { - if (!DeviceCriticalSection.TryEnter()) - { - Debug.LogMessage(LogEventLevel.Information, - "Currently unable to add devices to Device Manager. Please try again"); - return; - } - if (!AddDeviceEnabled) - { - Debug.LogMessage(LogEventLevel.Information, - "All devices have been activated. Adding new devices is not allowed."); - return; - } - - foreach (var dev in devicesToAdd) - { - try - { - Devices.Add(dev.Key, dev); - } - catch (ArgumentException ex) - { - Debug.LogMessage(LogEventLevel.Information, "Error adding device with key {0} to Device Manager: {1}\r\nStack Trace: {2}", - dev.Key, ex.Message, ex.StackTrace); - } - } - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - /// - /// RemoveDevice method - /// - public static void RemoveDevice(IKeyed newDev) - { - try - { - DeviceCriticalSection.Enter(); - if (newDev == null) - return; - if (Devices.ContainsKey(newDev.Key)) - Devices.Remove(newDev.Key); - //if (_Devices.Contains(newDev)) - // _Devices.Remove(newDev); - else - Debug.LogMessage(LogEventLevel.Information, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key); - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - /// - /// GetDeviceKeys method - /// - public static IEnumerable GetDeviceKeys() - { - //return _Devices.Select(d => d.Key).ToList(); - return Devices.Keys; - } - - /// - /// GetDevices method - /// - public static IEnumerable GetDevices() - { - //return _Devices.Select(d => d.Key).ToList(); - return Devices.Values; - } - - /// - /// GetDeviceForKey method - /// - public static IKeyed GetDeviceForKey(string key) - { - //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (key != null && Devices.ContainsKey(key)) - return Devices[key]; - - return null; - } - - /// - /// GetDeviceForKey method - /// - /// - public static T GetDeviceForKey(string key) - { - //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (key == null || !Devices.ContainsKey(key)) - return default; - - if (!(Devices[key] is T)) - { - Debug.LogMessage(LogEventLevel.Error, "Device with key '{0}' is not of type '{1}'", key, typeof(T).Name); - return default; - } - - return (T)Devices[key]; - } - - /// - /// Console handler that simulates com port data receive - /// - /// - /// - /// SimulateComReceiveOnDevice method - /// - public static void SimulateComReceiveOnDevice(string s) - { - // devcomsim:1 xyzabc - var match = Regex.Match(s, @"(\S*)\s*(.*)"); - if (match.Groups.Count < 3) - { - CrestronConsole.ConsoleCommandResponse(" Format: devsimreceive:P "); - return; - } - //Debug.LogMessage(LogEventLevel.Verbose, "**** {0} - {1} ****", match.Groups[1].Value, match.Groups[2].Value); - - if (!(GetDeviceForKey(match.Groups[1].Value) is ComPortController com)) - { - CrestronConsole.ConsoleCommandResponse("'{0}' is not a comm port device", match.Groups[1].Value); - return; - } - com.SimulateReceive(match.Groups[2].Value); - } - - /// - /// Prints a list of routing inputs and outputs by device key. - /// - /// Device key from which to report data - /// - /// GetRoutingPorts method - /// - public static void GetRoutingPorts(string s) - { - var device = GetDeviceForKey(s); - - if (device == null) return; - var inputPorts = ((device as IRoutingInputs) != null) ? (device as IRoutingInputs).InputPorts : null; - var outputPorts = ((device as IRoutingOutputs) != null) ? (device as IRoutingOutputs).OutputPorts : null; - if (inputPorts != null) - { - CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine); - foreach (var routingInputPort in inputPorts) - { - CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine); - } - } - if (outputPorts == null) return; - CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine); - foreach (var routingOutputPort in outputPorts) - { - CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine); - } - } - - /// - /// Attempts to set the debug level of a device - /// - /// - /// - /// SetDeviceStreamDebugging method - /// - public static void SetDeviceStreamDebugging(string s) - { - if (String.IsNullOrEmpty(s) || s.Contains("?")) - { - CrestronConsole.ConsoleCommandResponse( - "SETDEVICESTREAMDEBUG [{deviceKey}] [OFF |TX | RX | BOTH] [timeOutInMinutes]\r\n" + - " {deviceKey} [OFF | TX | RX | BOTH] - Device to set stream debugging on, and which setting to use\r\n" + - " timeOutInMinutes - Set timeout for stream debugging. Default is 30 minutes"); - return; - } - - var args = s.Split(' '); - - var deviceKey = args[0]; - var setting = args[1]; - - var timeout = String.Empty; - - if (args.Length >= 3) - { - timeout = args[2]; - } - - - if (!(GetDeviceForKey(deviceKey) is IStreamDebugging device)) - { - CrestronConsole.ConsoleCommandResponse("Unable to get device with key: {0}", deviceKey); - return; - } - - eStreamDebuggingSetting debugSetting; - - try - { - debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), setting, true); - } - catch - { - CrestronConsole.ConsoleCommandResponse("Unable to convert setting value. Please use off/rx/tx/both"); - return; - } - - if (!string.IsNullOrEmpty(timeout)) + DeviceCriticalSection.Enter(); + AddDeviceEnabled = false; + // PreActivate all devices + Debug.LogMessage(LogEventLevel.Information, "****PreActivation starting...****"); + foreach (var d in Devices.Values) { try { - var min = Convert.ToUInt32(timeout); - - device.StreamDebugging.SetDebuggingWithSpecificTimeout(debugSetting, min); - CrestronConsole.ConsoleCommandResponse("Device: '{0}' debug level set to {1} for {2} minutes", deviceKey, debugSetting, min); - + if (d is Device) + (d as Device).PreActivate(); } catch (Exception e) { - CrestronConsole.ConsoleCommandResponse("Unable to convert minutes or settings value. Please use an integer value for minutes. Error: {0}", e); + Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} PreActivation failure: {0}", e.Message, d.Key); + Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); } } - else + Debug.LogMessage(LogEventLevel.Information, "****PreActivation complete****"); + Debug.LogMessage(LogEventLevel.Information, "****Activation starting...****"); + + // Activate all devices + foreach (var d in Devices.Values) { - device.StreamDebugging.SetDebuggingWithDefaultTimeout(debugSetting); - CrestronConsole.ConsoleCommandResponse("Device: '{0}' debug level set to {1} for default time (30 minutes)", deviceKey, debugSetting); + try + { + if (d is Device) + (d as Device).Activate(); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} Activation failure: {0}", e.Message, d.Key); + Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); + } + } + + Debug.LogMessage(LogEventLevel.Information, "****Activation complete****"); + Debug.LogMessage(LogEventLevel.Information, "****PostActivation starting...****"); + + // PostActivate all devices + foreach (var d in Devices.Values) + { + try + { + if (d is Device) + (d as Device).PostActivate(); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} PostActivation failure: {0}", e.Message, d.Key); + Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); + } + } + + Debug.LogMessage(LogEventLevel.Information, "****PostActivation complete****"); + + OnAllDevicesActivated(); + } + finally + { + DeviceCriticalSection.Leave(); + } + } + + private static void DeviceManager_Initialized(object sender, EventArgs e) + { + var allInitialized = Devices.Values.OfType().All(d => d.IsInitialized); + + if (allInitialized) + { + Debug.LogMessage(LogEventLevel.Information, "****All Devices Initalized****"); + + OnAllDevicesInitialized(); + } + } + + private static void OnAllDevicesActivated() + { + var handler = AllDevicesActivated; + if (handler != null) + { + handler(null, new EventArgs()); + } + } + + private static void OnAllDevicesRegistered() + { + var handler = AllDevicesRegistered; + if (handler != null) + { + handler(null, new EventArgs()); + } + } + + private static void OnAllDevicesInitialized() + { + var handler = AllDevicesInitialized; + if (handler != null) + { + handler(null, new EventArgs()); + } + } + + /// + /// Calls activate on all Device class items + /// + public static void DeactivateAll() + { + try + { + DeviceCriticalSection.Enter(); + foreach (var d in Devices.Values.OfType()) + { + d.Deactivate(); } } - - /// - /// DisableAllDeviceStreamDebugging method - /// - public static void DisableAllDeviceStreamDebugging() + finally { - foreach (var device in AllDevices) + DeviceCriticalSection.Leave(); + } + } + + //static void ListMethods(string devKey) + //{ + // var dev = GetDeviceForKey(devKey); + // if(dev != null) + // { + // var type = dev.GetType().GetType(); + // var methods = type.GetMethods(BindingFlags.Public|BindingFlags.Instance); + // var sb = new StringBuilder(); + // sb.AppendLine(string.Format("{2} methods on [{0}] ({1}):", dev.Key, type.Name, methods.Length)); + // foreach (var m in methods) + // { + // sb.Append(string.Format("{0}(", m.Name)); + // var pars = m.GetParameters(); + // foreach (var p in pars) + // sb.Append(string.Format("({1}){0} ", p.Name, p.ParameterType.Name)); + // sb.AppendLine(")"); + // } + // CrestronConsole.ConsoleCommandResponse(sb.ToString()); + // } + //} + + private static void ListDevices(string s) + { + Debug.LogMessage(LogEventLevel.Information, "{0} Devices registered with Device Manager:", Devices.Count); + var sorted = Devices.Values.ToList(); + sorted.Sort((a, b) => a.Key.CompareTo(b.Key)); + + foreach (var d in sorted) + { + var name = d is IKeyName ? (d as IKeyName).Name : "---"; + Debug.LogMessage(LogEventLevel.Information, " [{0}] {1}", d.Key, name); + } + } + + private static void ListDeviceFeedbacks(string devKey) + { + var dev = GetDeviceForKey(devKey); + if (dev == null) + { + Debug.LogMessage(LogEventLevel.Information, "Device '{0}' not found", devKey); + return; + } + if (!(dev is IHasFeedback statusDev)) + { + Debug.LogMessage(LogEventLevel.Information, "Device '{0}' does not have visible feedbacks", devKey); + return; + } + statusDev.DumpFeedbacksToConsole(true); + } + + //static void ListDeviceCommands(string devKey) + //{ + // var dev = GetDeviceForKey(devKey); + // if (dev == null) + // { + // Debug.LogMessage(LogEventLevel.Information, "Device '{0}' not found", devKey); + // return; + // } + // Debug.LogMessage(LogEventLevel.Information, "This needs to be reworked. Stay tuned.", devKey); + //} + + private static void ListDeviceCommStatuses(string input) + { + + foreach (var dev in Devices.Values.OfType()) + { + CrestronConsole.ConsoleCommandResponse($"{dev}: {dev.CommunicationMonitor.Status}\r\n"); + } + } + + + //static void DoDeviceCommand(string command) + //{ + // Debug.LogMessage(LogEventLevel.Information, "Not yet implemented. Stay tuned"); + //} + + public static void AddDevice(IKeyed newDev) + { + try + { + if (!DeviceCriticalSection.TryEnter()) { - if (device is IStreamDebugging streamDevice) + Debug.LogMessage(LogEventLevel.Information, "Currently unable to add devices to Device Manager. Please try again"); + return; + } + // Check for device with same key + //var existingDevice = _Devices.FirstOrDefault(d => d.Key.Equals(newDev.Key, StringComparison.OrdinalIgnoreCase)); + ////// If it exists, remove or warn?? + //if (existingDevice != null) + + if (!AddDeviceEnabled) + { + Debug.LogMessage(LogEventLevel.Information, "All devices have been activated. Adding new devices is not allowed."); + return; + } + + if (Devices.ContainsKey(newDev.Key)) + { + Debug.LogMessage(LogEventLevel.Information, newDev, "WARNING: A device with this key already exists. Not added to manager"); + return; + } + Devices.Add(newDev.Key, newDev); + //if (!(_Devices.Contains(newDev))) + // _Devices.Add(newDev); + + if (newDev is EssentialsDevice essentialsDev) + essentialsDev.Initialized += DeviceManager_Initialized; + } + finally + { + DeviceCriticalSection.Leave(); + } + } + + public static void AddDevice(IEnumerable devicesToAdd) + { + try + { + if (!DeviceCriticalSection.TryEnter()) + { + Debug.LogMessage(LogEventLevel.Information, + "Currently unable to add devices to Device Manager. Please try again"); + return; + } + if (!AddDeviceEnabled) + { + Debug.LogMessage(LogEventLevel.Information, + "All devices have been activated. Adding new devices is not allowed."); + return; + } + + foreach (var dev in devicesToAdd) + { + try { - streamDevice.StreamDebugging.SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting.Off); + Devices.Add(dev.Key, dev); } + catch (ArgumentException ex) + { + Debug.LogMessage(LogEventLevel.Information, "Error adding device with key {0} to Device Manager: {1}\r\nStack Trace: {2}", + dev.Key, ex.Message, ex.StackTrace); + } + } + } + finally + { + DeviceCriticalSection.Leave(); + } + } + + public static void RemoveDevice(IKeyed newDev) + { + try + { + DeviceCriticalSection.Enter(); + if (newDev == null) + return; + if (Devices.ContainsKey(newDev.Key)) + Devices.Remove(newDev.Key); + //if (_Devices.Contains(newDev)) + // _Devices.Remove(newDev); + else + Debug.LogMessage(LogEventLevel.Information, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key); + } + finally + { + DeviceCriticalSection.Leave(); + } + } + + public static IEnumerable GetDeviceKeys() + { + //return _Devices.Select(d => d.Key).ToList(); + return Devices.Keys; + } + + public static IEnumerable GetDevices() + { + //return _Devices.Select(d => d.Key).ToList(); + return Devices.Values; + } + + /// + /// GetDeviceForKey method + /// + public static IKeyed GetDeviceForKey(string key) + { + //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (key != null && Devices.ContainsKey(key)) + return Devices[key]; + + return null; + } + + /// + /// GetDeviceForKey method + /// + /// + public static T GetDeviceForKey(string key) + { + //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (key == null || !Devices.ContainsKey(key)) + return default; + + if (!(Devices[key] is T)) + { + Debug.LogMessage(LogEventLevel.Error, "Device with key '{0}' is not of type '{1}'", key, typeof(T).Name); + return default; + } + + return (T)Devices[key]; + } + /// + /// Console handler that simulates com port data receive + /// + /// + public static void SimulateComReceiveOnDevice(string s) + { + // devcomsim:1 xyzabc + var match = Regex.Match(s, @"(\S*)\s*(.*)"); + if (match.Groups.Count < 3) + { + CrestronConsole.ConsoleCommandResponse(" Format: devsimreceive:P "); + return; + } + //Debug.LogMessage(LogEventLevel.Verbose, "**** {0} - {1} ****", match.Groups[1].Value, match.Groups[2].Value); + + if (!(GetDeviceForKey(match.Groups[1].Value) is ComPortController com)) + { + CrestronConsole.ConsoleCommandResponse("'{0}' is not a comm port device", match.Groups[1].Value); + return; + } + com.SimulateReceive(match.Groups[2].Value); + } + + /// + /// Prints a list of routing inputs and outputs by device key. + /// + /// Device key from which to report data + public static void GetRoutingPorts(string s) + { + var device = GetDeviceForKey(s); + + if (device == null) return; + var inputPorts = ((device as IRoutingInputs) != null) ? (device as IRoutingInputs).InputPorts : null; + var outputPorts = ((device as IRoutingOutputs) != null) ? (device as IRoutingOutputs).OutputPorts : null; + if (inputPorts != null) + { + CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine); + foreach (var routingInputPort in inputPorts) + { + CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine); + } + } + if (outputPorts == null) return; + CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine); + foreach (var routingOutputPort in outputPorts) + { + CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine); + } + } + + /// + /// Attempts to set the debug level of a device + /// + /// + public static void SetDeviceStreamDebugging(string s) + { + if (String.IsNullOrEmpty(s) || s.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse( + @"SETDEVICESTREAMDEBUG [{deviceKey}] [OFF |TX | RX | BOTH] [timeOutInMinutes] + {deviceKey} [OFF | TX | RX | BOTH] - Device to set stream debugging on, and which setting to use + timeOutInMinutes - Set timeout for stream debugging. Default is 30 minutes"); + return; + } + + var args = s.Split(' '); + + var deviceKey = args[0]; + var setting = args[1]; + + var timeout = String.Empty; + + if (args.Length >= 3) + { + timeout = args[2]; + } + + + if (!(GetDeviceForKey(deviceKey) is IStreamDebugging device)) + { + CrestronConsole.ConsoleCommandResponse("Unable to get device with key: {0}", deviceKey); + return; + } + + eStreamDebuggingSetting debugSetting; + + try + { + debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), setting, true); + } + catch + { + CrestronConsole.ConsoleCommandResponse("Unable to convert setting value. Please use off/rx/tx/both"); + return; + } + + if (!string.IsNullOrEmpty(timeout)) + { + try + { + var min = Convert.ToUInt32(timeout); + + device.StreamDebugging.SetDebuggingWithSpecificTimeout(debugSetting, min); + CrestronConsole.ConsoleCommandResponse("Device: '{0}' debug level set to {1} for {2} minutes", deviceKey, debugSetting, min); + + } + catch (Exception e) + { + CrestronConsole.ConsoleCommandResponse("Unable to convert minutes or settings value. Please use an integer value for minutes. Error: {0}", e); + } + } + else + { + device.StreamDebugging.SetDebuggingWithDefaultTimeout(debugSetting); + CrestronConsole.ConsoleCommandResponse("Device: '{0}' debug level set to {1} for default time (30 minutes)", deviceKey, debugSetting); + } + } + + /// + /// Sets stream debugging settings to off for all devices + /// + public static void DisableAllDeviceStreamDebugging() + { + foreach (var device in AllDevices) + { + if (device is IStreamDebugging streamDevice) + { + streamDevice.StreamDebugging.SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting.Off); } } } diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs index c79be592..6726e763 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -10,273 +10,266 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.ComponentModel.DataAnnotations; +using System.IO; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Provides functionality for managing and registering device factories, including loading plugin-based factories and +/// retrieving devices based on their configuration. +/// +/// The class is responsible for discovering and registering device factories +/// from plugins, as well as providing methods to retrieve devices based on their configuration. It maintains a +/// collection of factory methods that are keyed by device type names, allowing for extensibility through plugins. This +/// class also handles metadata retrieval and secret management for device configurations. +public class DeviceFactory { /// - /// Provides functionality for managing and registering device factories, including loading plugin-based factories and - /// retrieving devices based on their configuration. + /// Initializes a new instance of the class and loads all available device factories + /// from the current assembly. /// - /// The class is responsible for discovering and registering device factories - /// from plugins, as well as providing methods to retrieve devices based on their configuration. It maintains a - /// collection of factory methods that are keyed by device type names, allowing for extensibility through plugins. This - /// class also handles metadata retrieval and secret management for device configurations. - public class DeviceFactory - { - /// - /// Initializes a new instance of the class and loads all available device factories - /// from the current assembly. - /// - /// This constructor scans the executing assembly for types that implement the interface and are not abstract or interfaces. For each valid type, an instance is - /// created and passed to the LoadDeviceFactories method for further processing. If a type cannot be - /// instantiated, an informational log message is generated, and the process continues with the remaining - /// types. - public DeviceFactory() - { - var programAssemblies = Directory.GetFiles(InitialParametersClass.ProgramDirectory.ToString(), "*.dll"); + /// This constructor scans the executing assembly for types that implement the interface and are not abstract or interfaces. For each valid type, an instance is + /// created and passed to the LoadDeviceFactories method for further processing. If a type cannot be + /// instantiated, an informational log message is generated, and the process continues with the remaining + /// types. + public DeviceFactory() + { + var programAssemblies = Directory.GetFiles(InitialParametersClass.ProgramDirectory.ToString(), "*.dll"); - // Assemblies known to cause load errors that should be skipped - var assembliesToSkip = new[] { "CrestronOnvif.dll" }; - - foreach (var assembly in programAssemblies) - { - if (assembliesToSkip.Any(a => Path.GetFileName(assembly).Equals(a, StringComparison.OrdinalIgnoreCase))) - { - Debug.LogMessage(LogEventLevel.Verbose, "Skipping assembly: {assemblyName}", Path.GetFileName(assembly)); - continue; - } - - try - { - Assembly.LoadFrom(assembly); - } - catch (Exception e) - { - Debug.LogError("Unable to load assembly: {assemblyName} - {message}", assembly, e.Message); - } - } - - var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - - // Loop through all loaded assemblies that contain at least 1 type that implements IDeviceFactory - foreach (var assembly in loadedAssemblies) - { - Debug.LogDebug("loaded assembly: {assemblyName}", assembly.GetName()?.Name ?? "Unknown"); - - PluginLoader.AddLoadedAssembly(assembly.GetName()?.Name ?? "Unknown", assembly); - - var types = assembly.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); - - if (types == null || !types.Any()) - { - Debug.LogDebug("No DeviceFactory types found in assembly: {assemblyName}", assembly.GetName().Name); - continue; - } - - foreach (var type in types) - { - try - { - var factory = (IDeviceFactory)Activator.CreateInstance(type); - LoadDeviceFactories(factory); - } - catch (Exception e) - { - Debug.LogError("Unable to load type: '{message}' DeviceFactory: {type}", e.Message, type.Name); - } - } - - } - } - - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IDeviceFactory deviceFactory) - { - foreach (var typeName in deviceFactory.TypeNames) - { - string description = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0 - ? descriptionAttribute[0].Description - : "No description available"; - - AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); - } - } - - /// - /// A dictionary of factory methods, keyed by config types, added by plugins. - /// These methods are looked up and called by GetDevice in this class. - /// - private static readonly Dictionary FactoryMethods = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Registers a factory method for creating instances of a specific type. - /// - /// This method associates a type name with a factory method, allowing instances of the type to - /// be created dynamically. The factory method is stored internally and can be retrieved or invoked as - /// needed. - /// The name of the type for which the factory method is being registered. This value cannot be null or empty. - /// A delegate that defines the factory method. The delegate takes a parameter and - /// returns an instance of . - public static void AddFactoryForType(string typeName, Func method) - { - FactoryMethods.Add(typeName, new DeviceFactoryWrapper() { FactoryMethod = method }); - } - - /// - /// Registers a factory method for creating instances of a specific device type. - /// - /// If a factory method for the specified already exists, the method - /// will not overwrite it and will log an informational message instead. - /// The unique name of the device type. This serves as the key for identifying the factory method. - /// A brief description of the device type. This is used for informational purposes. - /// The of the device being registered. This represents the runtime type of the device. - /// A factory method that takes a as input and returns an instance of . - public static void AddFactoryForType(string typeName, string description, Type Type, Func method) - { - if (FactoryMethods.ContainsKey(typeName)) - { - Debug.LogInformation("Unable to add type: '{typeName}'. Already exists in DeviceFactory", typeName); - return; - } - - var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; - - FactoryMethods.Add(typeName, wrapper); - } - - private static void CheckForSecrets(IEnumerable obj) - { - foreach (var prop in obj.Where(prop => prop.Value as JObject != null)) - { - if (prop.Name.Equals("secret", StringComparison.CurrentCultureIgnoreCase)) - { - var secret = GetSecret(prop.Children().First().ToObject()); - - prop.Parent.Replace(secret); - } - - if (!(prop.Value is JObject recurseProp)) continue; - - CheckForSecrets(recurseProp.Properties()); - } - } - - private static string GetSecret(SecretsPropertiesConfig data) - { - var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); - if (secretProvider == null) return null; - var secret = secretProvider.GetSecret(data.Key); - if (secret != null) return (string)secret.Value; - Debug.LogMessage(LogEventLevel.Debug, - "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", - data.Provider, data.Key); - return string.Empty; - } - - - /// - /// Creates and returns a device instance based on the provided . - /// - /// This method attempts to create a device using the type specified in the - /// parameter. If the type corresponds to a registered factory method, the device is created and returned. If the - /// type is unrecognized or an exception occurs, the method logs the error and returns . - /// The configuration object containing the key, name, type, and properties required to create the device. - /// An instance of a device that implements , or if the device type is - /// not recognized or an error occurs during creation. - public static IKeyed GetDevice(DeviceConfig dc) + foreach(var assembly in programAssemblies) { try { - var localDc = new DeviceConfig(dc); - - var key = localDc.Key; - var name = localDc.Name; - var type = localDc.Type; - var properties = localDc.Properties; - - var typeName = localDc.Type.ToLower(); - - if (properties is JObject jObject) - { - var jProp = jObject.Properties(); - - CheckForSecrets(jProp); - } - - if (!FactoryMethods.TryGetValue(typeName, out var wrapper)) - { - Debug.LogWarning("Device type '{typeName}' not found in DeviceFactory", typeName); - return null; - } - - Debug.LogInformation("Loading '{type}' from {assemblyName}", typeName, wrapper.Type.Assembly.FullName); - - // Check for types that have been added by plugin dlls. - return wrapper.FactoryMethod(localDc); + Assembly.LoadFrom(assembly); } - catch (Exception ex) + catch (Exception e) { - Debug.LogError(ex, "Exception occurred while creating device {key}: {message}", dc.Key, ex.Message); - Debug.LogDebug(ex, "Exception details: {stackTrace}", ex.StackTrace); - return null; + Debug.LogError("Unable to load assembly: {assemblyName} - {message}", assembly, e.Message); } } - /// - /// Displays a list of device factory types that match the specified filter. - /// - /// The method outputs the filtered list of device factory types to the console, including their - /// key, type, and description. If a type is not specified by the plugin, it will be displayed as "Not Specified by - /// Plugin." - /// A string used to filter the device factory types by their keys. If the filter is null or empty, all device - /// factory types are displayed. - public static void GetDeviceFactoryTypes(string filter) + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + + // Loop through all loaded assemblies that contain at least 1 type that implements IDeviceFactory + foreach (var assembly in loadedAssemblies) { - var types = !string.IsNullOrEmpty(filter) - ? FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value) - : FactoryMethods; + Debug.LogDebug("loaded assembly: {assemblyName}", assembly.GetName().Name); - CrestronConsole.ConsoleCommandResponse("Device Types:"); + PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly); - foreach (var type in types.OrderBy(t => t.Key)) + var types = assembly.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + + if(types == null || !types.Any()) { - var description = type.Value.Description; - var Type = "Not Specified by Plugin"; - - if (type.Value.Type != null) - { - Type = type.Value.Type.FullName; - } - - CrestronConsole.ConsoleCommandResponse( - "Type: '{0}'\r\n" + - " Type: '{1}'\r\n" + - " Description: {2}{3}", type.Key, Type, description, CrestronEnvironment.NewLine); + Debug.LogDebug("No DeviceFactory types found in assembly: {assemblyName}", assembly.GetName().Name); + continue; } - } - /// - /// Retrieves a dictionary of device factory wrappers, optionally filtered by a specified string. - /// - /// A string used to filter the dictionary keys. Only entries with keys containing the specified filter will be - /// included. If or empty, all entries are returned. - /// A dictionary where the keys are strings representing device identifiers and the values are instances. The dictionary may be empty if no entries match the filter. - public static Dictionary GetDeviceFactoryDictionary(string filter) + foreach (var type in types) + { + try + { + var factory = (IDeviceFactory)Activator.CreateInstance(type); + LoadDeviceFactories(factory); + } + catch (Exception e) + { + Debug.LogError("Unable to load type: '{message}' DeviceFactory: {type}", e.Message, type.Name); + } + } + + } + } + + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) { - return string.IsNullOrEmpty(filter) - ? FactoryMethods - : FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value); + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } + + /// + /// A dictionary of factory methods, keyed by config types, added by plugins. + /// These methods are looked up and called by GetDevice in this class. + /// + private static readonly Dictionary FactoryMethods = + new(StringComparer.OrdinalIgnoreCase); + + /// + /// Registers a factory method for creating instances of a specific type. + /// + /// This method associates a type name with a factory method, allowing instances of the type to + /// be created dynamically. The factory method is stored internally and can be retrieved or invoked as + /// needed. + /// The name of the type for which the factory method is being registered. This value cannot be null or empty. + /// A delegate that defines the factory method. The delegate takes a parameter and + /// returns an instance of . + public static void AddFactoryForType(string typeName, Func method) + { + FactoryMethods.Add(typeName, new DeviceFactoryWrapper() { FactoryMethod = method}); + } + + /// + /// Registers a factory method for creating instances of a specific device type. + /// + /// If a factory method for the specified already exists, the method + /// will not overwrite it and will log an informational message instead. + /// The unique name of the device type. This serves as the key for identifying the factory method. + /// A brief description of the device type. This is used for informational purposes. + /// The of the device being registered. This represents the runtime type of the device. + /// A factory method that takes a as input and returns an instance of . + public static void AddFactoryForType(string typeName, string description, Type Type, Func method) + { + if(FactoryMethods.ContainsKey(typeName)) + { + Debug.LogInformation("Unable to add type: '{typeName}'. Already exists in DeviceFactory", typeName); + return; + } + + var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; + + FactoryMethods.Add(typeName, wrapper); + } + + private static void CheckForSecrets(IEnumerable obj) + { + foreach (var prop in obj.Where(prop => prop.Value as JObject != null)) + { + if (prop.Name.Equals("secret", StringComparison.CurrentCultureIgnoreCase)) + { + var secret = GetSecret(prop.Children().First().ToObject()); + + prop.Parent.Replace(secret); + } + if (prop.Value is not JObject recurseProp) return; + CheckForSecrets(recurseProp.Properties()); + } + } + + private static string GetSecret(SecretsPropertiesConfig data) + { + var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); + if (secretProvider == null) return null; + var secret = secretProvider.GetSecret(data.Key); + if (secret != null) return (string) secret.Value; + Debug.LogMessage(LogEventLevel.Debug, + "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", + data.Provider, data.Key); + return string.Empty; + } + + + /// + /// Creates and returns a device instance based on the provided . + /// + /// This method attempts to create a device using the type specified in the + /// parameter. If the type corresponds to a registered factory method, the device is created and returned. If the + /// type is unrecognized or an exception occurs, the method logs the error and returns . + /// The configuration object containing the key, name, type, and properties required to create the device. + /// An instance of a device that implements , or if the device type is + /// not recognized or an error occurs during creation. + public static IKeyed GetDevice(DeviceConfig dc) + { + try + { + var localDc = new DeviceConfig(dc); + + var key = localDc.Key; + var name = localDc.Name; + var type = localDc.Type; + var properties = localDc.Properties; + + var typeName = localDc.Type.ToLower(); + + if (properties is JObject jObject) + { + var jProp = jObject.Properties(); + + CheckForSecrets(jProp); + } + + if (!FactoryMethods.TryGetValue(typeName, out var wrapper)) + { + Debug.LogWarning("Device type '{typeName}' not found in DeviceFactory", typeName); + return null; + } + + Debug.LogInformation("Loading '{type}' from {assemblyName}", typeName, wrapper.Type.Assembly.FullName); + + // Check for types that have been added by plugin dlls. + return wrapper.FactoryMethod(localDc); + } + catch (Exception ex) + { + Debug.LogError(ex, "Exception occurred while creating device {0}: {1}", null, dc.Key, ex.Message); + return null; + } + } + + /// + /// Displays a list of device factory types that match the specified filter. + /// + /// The method outputs the filtered list of device factory types to the console, including their + /// key, type, and description. If a type is not specified by the plugin, it will be displayed as "Not Specified by + /// Plugin." + /// A string used to filter the device factory types by their keys. If the filter is null or empty, all device + /// factory types are displayed. + public static void GetDeviceFactoryTypes(string filter) + { + var types = !string.IsNullOrEmpty(filter) + ? FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value) + : FactoryMethods; + + CrestronConsole.ConsoleCommandResponse("Device Types:"); + + foreach (var type in types.OrderBy(t => t.Key)) + { + var description = type.Value.Description; + var Type = "Not Specified by Plugin"; + + if (type.Value.Type != null) + { + Type = type.Value.Type.FullName; + } + + CrestronConsole.ConsoleCommandResponse( + @"Type: '{0}' + Type: '{1}' + Description: {2}{3}", type.Key, Type, description, CrestronEnvironment.NewLine); + } + } + + /// + /// Retrieves a dictionary of device factory wrappers, optionally filtered by a specified string. + /// + /// A string used to filter the dictionary keys. Only entries with keys containing the specified filter will be + /// included. If or empty, all entries are returned. + /// A dictionary where the keys are strings representing device identifiers and the values are instances. The dictionary may be empty if no entries match the filter. + public static Dictionary GetDeviceFactoryDictionary(string filter) + { + return string.IsNullOrEmpty(filter) + ? FactoryMethods + : FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value); + } } diff --git a/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs index eb02f3e5..5a082209 100644 --- a/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs @@ -10,160 +10,143 @@ using System.Linq; namespace PepperDash.Essentials.Core; +public class ProcessorExtensionDeviceFactory +{ + public ProcessorExtensionDeviceFactory() { + var assy = Assembly.GetExecutingAssembly(); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); + + var extensions = assy.GetTypes().Where(ct => typeof(IProcessorExtensionDeviceFactory) + .IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + + if (extensions != null ) + { + foreach ( var extension in extensions ) + { + try + { + var factory = (IProcessorExtensionDeviceFactory)Activator.CreateInstance(extension); + factory.LoadFactories(); + } + catch( Exception e ) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to load extension device: '{1}' ProcessorExtensionDeviceFactory: {0}", e, extension.Name); + } + } + } + } + /// - /// Represents a ProcessorExtensionDeviceFactory + /// A dictionary of factory methods, keyed by config types, added by plugins. + /// These methods are looked up and called by GetDevice in this class. /// - public class ProcessorExtensionDeviceFactory + static Dictionary ProcessorExtensionFactoryMethods = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + + /// + /// Adds a plugin factory method + /// + /// + /// + public static void AddFactoryForType(string extensionName, Func method) { - /// - /// Constructor - /// - public ProcessorExtensionDeviceFactory() { - var assy = Assembly.GetExecutingAssembly(); - PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); + //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); + ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods.Add(extensionName, new DeviceFactoryWrapper() { FactoryMethod = method }); + } - var extensions = assy.GetTypes().Where(ct => typeof(IProcessorExtensionDeviceFactory) - .IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + public static void AddFactoryForType(string extensionName, string description, Type Type, Func method) + { + //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); - if (extensions != null ) + if (ProcessorExtensionFactoryMethods.ContainsKey(extensionName)) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to add extension device: '{0}'. Already exists in ProcessorExtensionDeviceFactory", extensionName); + return; + } + + var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; + ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods.Add(extensionName, wrapper); + } + + private static void CheckForSecrets(IEnumerable obj) + { + foreach (var prop in obj.Where(prop => prop.Value as Newtonsoft.Json.Linq.JObject != null)) + { + if (prop.Name.ToLower() == "secret") { - foreach ( var extension in extensions ) - { - try - { - var factory = (IProcessorExtensionDeviceFactory)Activator.CreateInstance(extension); - factory.LoadFactories(); - } - catch( Exception e ) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to load extension device: '{1}' ProcessorExtensionDeviceFactory: {0}", e, extension.Name); - } - } + var secret = GetSecret(prop.Children().First().ToObject()); + //var secret = GetSecret(JsonConvert.DeserializeObject(prop.Children().First().ToString())); + prop.Parent.Replace(secret); } + var recurseProp = prop.Value as Newtonsoft.Json.Linq.JObject; + if (recurseProp == null) return; + CheckForSecrets(recurseProp.Properties()); } + } - /// - /// A dictionary of factory methods, keyed by config types, added by plugins. - /// These methods are looked up and called by GetDevice in this class. - /// - static Dictionary ProcessorExtensionFactoryMethods = - new Dictionary(StringComparer.OrdinalIgnoreCase); + private static string GetSecret(SecretsPropertiesConfig data) + { + var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); + if (secretProvider == null) return null; + var secret = secretProvider.GetSecret(data.Key); + if (secret != null) return (string)secret.Value; + Debug.LogMessage(LogEventLevel.Debug, + "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", + data.Provider, data.Key); + return String.Empty; + } - - /// - /// Adds a plugin factory method - /// - /// name fo extension to add - /// method to add - /// - public static void AddFactoryForType(string extensionName, Func method) + /// + /// The factory method for processor extension devices. Also iterates the Factory methods that have + /// been loaded from plugins + /// + /// + /// + public static IKeyed GetExtensionDevice(DeviceConfig dc) + { + try { - //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); - ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods.Add(extensionName, new DeviceFactoryWrapper() { FactoryMethod = method }); - } + Debug.LogMessage(LogEventLevel.Information, "Loading '{0}' from Essentials Core", dc.Type); - /// - /// Adds a plugin factory method with type and description - /// - /// name of extension to add - /// description of extension to add - /// type of extension to add - /// method to add - public static void AddFactoryForType(string extensionName, string description, Type Type, Func method) - { - //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); + var localDc = new DeviceConfig(dc); - if (ProcessorExtensionFactoryMethods.ContainsKey(extensionName)) + var key = localDc.Key; + var name = localDc.Name; + var type = localDc.Type; + var properties = localDc.Properties; + //var propRecurse = properties; + + var typeName = localDc.Type.ToLower(); + + var jObject = properties as Newtonsoft.Json.Linq.JObject; + if (jObject != null) { - Debug.LogMessage(LogEventLevel.Information, "Unable to add extension device: '{0}'. Already exists in ProcessorExtensionDeviceFactory", extensionName); - return; + var jProp = jObject.Properties(); + + CheckForSecrets(jProp); } - var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; - ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods.Add(extensionName, wrapper); + Debug.LogMessage(LogEventLevel.Verbose, "typeName = {0}", typeName); + // Check for types that have been added by plugin dlls. + return !ProcessorExtensionFactoryMethods.ContainsKey(typeName) ? null : ProcessorExtensionFactoryMethods[typeName].FactoryMethod(localDc); } - - private static void CheckForSecrets(IEnumerable obj) + catch (Exception ex) { - foreach (var prop in obj.Where(prop => prop.Value as Newtonsoft.Json.Linq.JObject != null)) + Debug.LogMessage(LogEventLevel.Information, "Exception occurred while creating device {0}: {1}", dc.Key, ex.Message); + + Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.StackTrace); + + if (ex.InnerException == null) { - if (prop.Name.ToLower() == "secret") - { - var secret = GetSecret(prop.Children().First().ToObject()); - //var secret = GetSecret(JsonConvert.DeserializeObject(prop.Children().First().ToString())); - prop.Parent.Replace(secret); - } - var recurseProp = prop.Value as Newtonsoft.Json.Linq.JObject; - if (recurseProp == null) return; - CheckForSecrets(recurseProp.Properties()); - } - } - - private static string GetSecret(SecretsPropertiesConfig data) - { - var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); - if (secretProvider == null) return null; - var secret = secretProvider.GetSecret(data.Key); - if (secret != null) return (string)secret.Value; - Debug.LogMessage(LogEventLevel.Debug, - "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", - data.Provider, data.Key); - return String.Empty; - } - - /// - /// The factory method for processor extension devices. Also iterates the Factory methods that have - /// been loaded from plugins - /// - /// - /// - /// - /// GetExtensionDevice method - /// - public static IKeyed GetExtensionDevice(DeviceConfig dc) - { - try - { - Debug.LogMessage(LogEventLevel.Information, "Loading '{0}' from Essentials Core", dc.Type); - - var localDc = new DeviceConfig(dc); - - var key = localDc.Key; - var name = localDc.Name; - var type = localDc.Type; - var properties = localDc.Properties; - //var propRecurse = properties; - - var typeName = localDc.Type.ToLower(); - - var jObject = properties as Newtonsoft.Json.Linq.JObject; - if (jObject != null) - { - var jProp = jObject.Properties(); - - CheckForSecrets(jProp); - } - - Debug.LogMessage(LogEventLevel.Verbose, "typeName = {0}", typeName); - // Check for types that have been added by plugin dlls. - return !ProcessorExtensionFactoryMethods.ContainsKey(typeName) ? null : ProcessorExtensionFactoryMethods[typeName].FactoryMethod(localDc); - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Information, "Exception occurred while creating device {0}: {1}", dc.Key, ex.Message); - - Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.StackTrace); - - if (ex.InnerException == null) - { - return null; - } - - Debug.LogMessage(LogEventLevel.Information, "Inner exception while creating device {0}: {1}", dc.Key, - ex.InnerException.Message); - Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.InnerException.StackTrace); return null; } + + Debug.LogMessage(LogEventLevel.Information, "Inner exception while creating device {0}: {1}", dc.Key, + ex.InnerException.Message); + Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.InnerException.StackTrace); + return null; + } } } diff --git a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs index 3a3117cb..53f3927f 100644 --- a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs @@ -7,58 +7,51 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common +namespace PepperDash.Essentials.Devices.Common; + +public class DeviceFactory { - /// - /// Represents a DeviceFactory - /// - public class DeviceFactory + + public DeviceFactory() { - /// - /// Initializes a new instance of the DeviceFactory class - /// - public DeviceFactory() + var assy = Assembly.GetExecutingAssembly(); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); + + var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + + if (types != null) { - var assy = Assembly.GetExecutingAssembly(); - PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); - - var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); - - if (types != null) + foreach (var type in types) { - foreach (var type in types) + try { - try - { - var factory = (IDeviceFactory)Activator.CreateInstance(type); - LoadDeviceFactories(factory); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to load type: '{1}' DeviceFactory: {0}", e, type.Name); - } + var factory = (IDeviceFactory)Activator.CreateInstance(type); + LoadDeviceFactories(factory); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to load type: '{1}' DeviceFactory: {0}", e, type.Name); } } } + } - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) { - foreach (var typeName in deviceFactory.TypeNames) - { - //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); - string description = (deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description - : "No description available"; - - Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); - } + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index 9ce584f3..cba0afd5 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -4,61 +4,54 @@ using System.Reflection; using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +public class MobileControlFactory { - /// - /// Factory class for the Mobile Control App Controller - /// - public class MobileControlFactory + public MobileControlFactory() { - /// - /// Create an instance of the class. - /// - public MobileControlFactory() + var assembly = Assembly.GetExecutingAssembly(); + + PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly); + + var types = assembly.GetTypes().Where(t => typeof(IDeviceFactory).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); + + if (types == null) { - var assembly = Assembly.GetExecutingAssembly(); - - PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly); - - var types = assembly.GetTypes().Where(t => typeof(IDeviceFactory).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); - - if (types == null) - { - return; - } - - foreach (var type in types) - { - try - { - var factory = (IDeviceFactory)Activator.CreateInstance(type); - - LoadDeviceFactories(factory); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Unable to load type '{type}' DeviceFactory: {factory}", null, type.Name); - } - } + return; } - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + foreach (var type in types) { - foreach (var typeName in deviceFactory.TypeNames) + try { - string description = (deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description - : "No description available"; // Default value if no DescriptionAttribute is found + var factory = (IDeviceFactory)Activator.CreateInstance(type); - DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); + LoadDeviceFactories(factory); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Unable to load type '{type}' DeviceFactory: {factory}", null, type.Name); } } } + + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); + } + } } diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 608c362f..be37ff21 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -12,273 +12,269 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Routing; +using System.Threading; +using Timeout = Crestron.SimplSharp.Timeout; using Serilog.Events; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +/// +/// Represents the main control system for the application, providing initialization, configuration loading, and device +/// management functionality. +/// +/// This class extends and serves as the entry point for the control +/// system. It manages the initialization of devices, rooms, tie lines, and other system components. Additionally, it +/// provides methods for platform determination, configuration loading, and system teardown. +public class ControlSystem : CrestronControlSystem, ILoadConfig { + private HttpLogoServer LogoServer; + + private Timer _startTimer; + private ManualResetEventSlim _initializeEvent; + private const long StartupTime = 500; + /// - /// Main control system class that inherits from CrestronControlSystem and manages program lifecycle + /// Initializes a new instance of the class, setting up the system's global state and + /// dependencies. /// - public class ControlSystem : CrestronControlSystem, ILoadConfig + /// This constructor configures the control system by initializing key components such as the + /// device manager and secrets manager, and sets global properties like the maximum number of user threads and the + /// program initialization state. It also adjusts the error log's minimum debug level based on the device + /// platform. + public ControlSystem() + : base() + { - HttpLogoServer LogoServer; - - private CTimer _startTimer; - private CEvent _initializeEvent; - private const long StartupTime = 500; - - /// - /// Initializes a new instance of the ControlSystem class - /// - public ControlSystem() - : base() - + try { - - Thread.MaxNumberOfUserThreads = 400; + Crestron.SimplSharpPro.CrestronThread.Thread.MaxNumberOfUserThreads = 400; + Global.ControlSystem = this; DeviceManager.Initialize(this); SecretsManager.Initialize(); SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true; Debug.SetErrorLogMinimumDebugLevel(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? LogEventLevel.Warning : LogEventLevel.Verbose); - - // AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; + } + catch (Exception e) + { + Debug.LogError(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); + } + } + + /// + /// Initializes the control system and prepares it for operation. + /// + /// This method ensures that all devices in the system are properly registered and initialized + /// before the system is fully operational. If the control system is of a DMPS type, the method waits for all + /// devices to activate, allowing HD-BaseT DM endpoints to register before completing initialization. For non-DMPS + /// systems, initialization proceeds without waiting. + public override void InitializeSystem() + { + // If the control system is a DMPS type, we need to wait to exit this method until all devices have had time to activate + // to allow any HD-BaseT DM endpoints to register first. + bool preventInitializationComplete = Global.ControlSystemIsDmpsType; + if (preventInitializationComplete) + { + Debug.LogMessage(LogEventLevel.Debug, "******************* Initializing System **********************"); + + _startTimer = new Timer(StartSystem, preventInitializationComplete, StartupTime, Timeout.Infinite); + + _initializeEvent = new ManualResetEventSlim(false); + + DeviceManager.AllDevicesRegistered += (o, a) => + { + _initializeEvent.Set(); + }; + + _initializeEvent.Wait(30000); + + Debug.LogMessage(LogEventLevel.Debug, "******************* System Initialization Complete **********************"); + + SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; + } + else + { + _startTimer = new Timer(StartSystem, preventInitializationComplete, StartupTime, Timeout.Infinite); + } + } + + private void StartSystem(object preventInitialization) + { + DeterminePlatform(); + + // Print .NET runtime version + Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); + + if (Debug.DoNotLoadConfigOnNextBoot) + { + CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file", + ConsoleAccessLevelEnum.AccessOperator); } - private System.Reflection.Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) + CrestronConsole.AddNewConsoleCommand(PluginLoader.ReportAssemblyVersions, "reportversions", "Reports the versions of the loaded assemblies", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(Core.DeviceFactory.GetDeviceFactoryTypes, "gettypes", "Gets the device types that can be built. Accepts a filter string.", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(BridgeHelper.PrintJoinMap, "getjoinmap", "map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(BridgeHelper.JoinmapMarkdown, "getjoinmapmarkdown" + , "generate markdown of map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => { - var assemblyName = new System.Reflection.AssemblyName(args.Name).Name; - if (assemblyName == "PepperDash_Core") - { - return System.Reflection.Assembly.LoadFrom("PepperDashCore.dll"); - } + foreach (var tl in TieLineCollection.Default) + CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); + }, + "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); - if (assemblyName == "PepperDash_Essentials_Core") - { - return System.Reflection.Assembly.LoadFrom("PepperDash.Essentials.Core.dll"); - } + CrestronConsole.AddNewConsoleCommand(s => + { + 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)); + }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); - if (assemblyName == "Essentials Devices Common") - { - return System.Reflection.Assembly.LoadFrom("PepperDash.Essentials.Devices.Common.dll"); - } + CrestronConsole.AddNewConsoleCommand(s => + CrestronConsole.ConsoleCommandResponse( + "This system can be found at the following URLs:{2}" + + "System URL: {0}{2}" + + "Template URL: {1}{2}", + ConfigReader.ConfigObject.SystemUrl, + ConfigReader.ConfigObject.TemplateUrl, + CrestronEnvironment.NewLine), + "portalinfo", + "Shows portal URLS from configuration", + ConsoleAccessLevelEnum.AccessOperator); - return null; + + CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, + "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); + + + if (!Debug.DoNotLoadConfigOnNextBoot) + { + GoWithLoad(); + return; } - /// - /// InitializeSystem method - /// - /// - public override void InitializeSystem() + if (!(bool)preventInitialization) { - // If the control system is a DMPS type, we need to wait to exit this method until all devices have had time to activate - // to allow any HD-BaseT DM endpoints to register first. - bool preventInitializationComplete = Global.ControlSystemIsDmpsType; - if (preventInitializationComplete) - { - Debug.LogMessage(LogEventLevel.Debug, "******************* InitializeSystem() Entering **********************"); + SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; + } + } - _startTimer = new CTimer(StartSystem, preventInitializationComplete, StartupTime); - _initializeEvent = new CEvent(true, false); - DeviceManager.AllDevicesRegistered += (o, a) => + /// + /// Determines if the program is running on a processor (appliance) or server (VC-4). + /// + /// Sets Global.FilePathPrefix and Global.ApplicationDirectoryPathPrefix based on platform + /// + public void DeterminePlatform() + { + try + { + Debug.LogMessage(LogEventLevel.Information, "Determining Platform..."); + + string filePathPrefix; + + var dirSeparator = Global.DirectorySeparator; + + string directoryPrefix; + + directoryPrefix = Directory.GetApplicationRootDirectory(); + + Global.SetAssemblyVersion(PluginLoader.GetAssemblyVersion(Assembly.GetExecutingAssembly())); + + if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Server) // Handles 3-series running Windows CE OS + { + string userFolder = "user"; + string nvramFolder = "nvram"; + + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, "4-series"); + //Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{0} on {1} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series"); + + // Check if User/ProgramX exists + if (Directory.Exists(Global.ApplicationDirectoryPathPrefix + dirSeparator + userFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber))) { - _initializeEvent.Set(); - }; - _initializeEvent.Wait(30000); - Debug.LogMessage(LogEventLevel.Debug, "******************* InitializeSystem() Exiting **********************"); - SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; + Debug.LogMessage(LogEventLevel.Information, "{userFolder:l}/program{applicationNumber} directory found", userFolder, InitialParametersClass.ApplicationNumber); + filePathPrefix = directoryPrefix + dirSeparator + userFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; + } + // Check if Nvram/Programx exists + else if (Directory.Exists(directoryPrefix + dirSeparator + nvramFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber))) + { + Debug.LogMessage(LogEventLevel.Information, "{nvramFolder:l}/program{applicationNumber} directory found", nvramFolder, InitialParametersClass.ApplicationNumber); + + filePathPrefix = directoryPrefix + dirSeparator + nvramFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; + } + // If neither exists, set path to User/ProgramX + else + { + Debug.LogMessage(LogEventLevel.Information, "{userFolder:l}/program{applicationNumber} directory found", userFolder, InitialParametersClass.ApplicationNumber); + + filePathPrefix = directoryPrefix + dirSeparator + userFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; + } + } + else // Handles Linux OS (Virtual Control) + { + //Debug.SetDebugLevel(2); + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on Virtual Control Server", Global.AssemblyVersion); + + // Set path to User/ + filePathPrefix = directoryPrefix + dirSeparator + "User" + dirSeparator; + } + + Global.SetFilePathPrefix(filePathPrefix); + } + catch (Exception e) + { + Debug.LogMessage(e, "Unable to determine platform due to exception"); + } + } + + /// + /// Begins the process of loading resources including plugins and configuration data + /// + public void GoWithLoad() + { + try + { + Debug.SetDoNotLoadConfigOnNextBoot(false); + + PluginLoader.AddProgramAssemblies(); + + _ = new Core.DeviceFactory(); + + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); + + var filesReady = SetupFilesystem(); + if (filesReady) + { + Debug.LogMessage(LogEventLevel.Information, "Checking for plugins"); + PluginLoader.LoadPlugins(); + + Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config..."); + if (!ConfigReader.LoadConfig2()) + { + Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors"); + return; + } + + Load(); + Debug.LogMessage(LogEventLevel.Information, "Essentials load complete"); } else { - _startTimer = new CTimer(StartSystem, preventInitializationComplete, StartupTime); - } - } - - private void StartSystem(object preventInitialization) - { - DeterminePlatform(); - - // Print .NET runtime version - Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); - - if (Debug.DoNotLoadConfigOnNextBoot) - { - CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file", - ConsoleAccessLevelEnum.AccessOperator); - } - - CrestronConsole.AddNewConsoleCommand(PluginLoader.ReportAssemblyVersions, "reportversions", "Reports the versions of the loaded assemblies", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(Core.DeviceFactory.GetDeviceFactoryTypes, "gettypes", "Gets the device types that can be built. Accepts a filter string.", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(BridgeHelper.PrintJoinMap, "getjoinmap", "map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(BridgeHelper.JoinmapMarkdown, "getjoinmapmarkdown" - , "generate markdown of map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => - { - foreach (var tl in TieLineCollection.Default) - CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); - }, - "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => - { - 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).Replace(Environment.NewLine, "\r\n")); - }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => - CrestronConsole.ConsoleCommandResponse( - "This system can be found at the following URLs:{2}" + - "System URL: {0}{2}" + - "Template URL: {1}{2}", - ConfigReader.ConfigObject.SystemUrl, - ConfigReader.ConfigObject.TemplateUrl, - CrestronEnvironment.NewLine), - "portalinfo", - "Shows portal URLS from configuration", - ConsoleAccessLevelEnum.AccessOperator); - - - CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, - "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); - - //DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); - - if (!Debug.DoNotLoadConfigOnNextBoot) - { - GoWithLoad(); - return; - } - - if (!(bool)preventInitialization) - { - SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; - } - } - - /// - /// DeterminePlatform method - /// - public void DeterminePlatform() - { - try - { - Debug.LogMessage(LogEventLevel.Information, "Determining Platform..."); - - string filePathPrefix; - - var dirSeparator = Global.DirectorySeparator; - - string directoryPrefix; - - directoryPrefix = Directory.GetApplicationRootDirectory(); - - Global.SetAssemblyVersion(PluginLoader.GetAssemblyVersion(Assembly.GetExecutingAssembly())); - - if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Server) // Handles 3-series running Windows CE OS - { - string userFolder = "user"; - string nvramFolder = "nvram"; - - Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, "4-series"); - //Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{0} on {1} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series"); - - // Check if User/ProgramX exists - if (Directory.Exists(Global.ApplicationDirectoryPathPrefix + dirSeparator + userFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber))) - { - - Debug.LogMessage(LogEventLevel.Information, "{userFolder:l}/program{applicationNumber} directory found", userFolder, InitialParametersClass.ApplicationNumber); - filePathPrefix = directoryPrefix + dirSeparator + userFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; - } - // Check if Nvram/Programx exists - else if (Directory.Exists(directoryPrefix + dirSeparator + nvramFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber))) - { - Debug.LogMessage(LogEventLevel.Information, "{nvramFolder:l}/program{applicationNumber} directory found", nvramFolder, InitialParametersClass.ApplicationNumber); - - filePathPrefix = directoryPrefix + dirSeparator + nvramFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; - } - // If neither exists, set path to User/ProgramX - else - { - Debug.LogMessage(LogEventLevel.Information, "{userFolder:l}/program{applicationNumber} directory found", userFolder, InitialParametersClass.ApplicationNumber); - - filePathPrefix = directoryPrefix + dirSeparator + userFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; - } - } - else // Handles Linux OS (Virtual Control) - { - //Debug.SetDebugLevel(2); - Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on Virtual Control Server", Global.AssemblyVersion); - - // Set path to User/ - filePathPrefix = directoryPrefix + dirSeparator + "User" + dirSeparator; - } - - Global.SetFilePathPrefix(filePathPrefix); - } - catch (Exception e) - { - Debug.LogMessage(e, "Unable to determine platform due to exception"); - } - } - - /// - /// GoWithLoad method - /// - public void GoWithLoad() - { - try - { - Debug.SetDoNotLoadConfigOnNextBoot(false); - - PluginLoader.AddProgramAssemblies(); - - _ = new Core.DeviceFactory(); - // _ = new Devices.Common.DeviceFactory(); - // _ = new DeviceFactory(); - - // _ = new ProcessorExtensionDeviceFactory(); - // _ = new MobileControlFactory(); - - LoadAssets(); - - Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); - - var filesReady = SetupFilesystem(); - if (filesReady) - { - Debug.LogMessage(LogEventLevel.Information, "Checking for plugins"); - PluginLoader.LoadPlugins(); - - Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config..."); - if (!ConfigReader.LoadConfig2()) - { - Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors"); - return; - } - - Load(); - Debug.LogMessage(LogEventLevel.Information, "Essentials load complete"); - } - else - { - Debug.LogMessage(LogEventLevel.Information, - @"---------------------------------------------- + Debug.LogMessage(LogEventLevel.Information, + @"---------------------------------------------- ------------------------------------------------ ------------------------------------------------ Essentials file structure setup completed. @@ -287,415 +283,415 @@ namespace PepperDash.Essentials ------------------------------------------------ ------------------------------------------------ ------------------------------------------------"); - } - - } - catch (Exception e) - { - Debug.LogMessage(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); - } - finally - { - // Notify the OS that the program intitialization has completed - SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; } } - - - - /// - /// Verifies filesystem is set up. IR, SGD, and programX folders - /// - bool SetupFilesystem() + catch (Exception e) { - Debug.LogMessage(LogEventLevel.Information, "Verifying and/or creating folder structure"); - var configDir = Global.FilePathPrefix; - - Debug.LogMessage(LogEventLevel.Information, "FilePathPrefix: {filePathPrefix:l}", configDir); - var configExists = Directory.Exists(configDir); - if (!configExists) - Directory.Create(configDir); - - var irDir = Global.FilePathPrefix + "ir"; - if (!Directory.Exists(irDir)) - Directory.Create(irDir); - - var sgdDir = Global.FilePathPrefix + "sgd"; - if (!Directory.Exists(sgdDir)) - Directory.Create(sgdDir); - - var pluginDir = Global.FilePathPrefix + "plugins"; - if (!Directory.Exists(pluginDir)) - Directory.Create(pluginDir); - - var joinmapDir = Global.FilePathPrefix + "joinmaps"; - if (!Directory.Exists(joinmapDir)) - Directory.Create(joinmapDir); - - return configExists; + Debug.LogMessage(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); + } + finally + { + // Notify the OS that the program intitialization has completed + SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; } - /// - /// TearDown method - /// - public void TearDown() - { - Debug.LogMessage(LogEventLevel.Information, "Tearing down existing system"); - DeviceManager.DeactivateAll(); - - TieLineCollection.Default.Clear(); - - foreach (var key in DeviceManager.GetDevices()) - DeviceManager.RemoveDevice(key); - - Debug.LogMessage(LogEventLevel.Information, "Tear down COMPLETE"); - } + } - /// - /// Load method - /// - void Load() - { - LoadDevices(); - LoadRooms(); - LoadLogoServer(); - DeviceManager.ActivateAll(); + /// + /// Verifies filesystem is set up. IR, SGD, and programX folders + /// + bool SetupFilesystem() + { + Debug.LogMessage(LogEventLevel.Information, "Verifying and/or creating folder structure"); + var configDir = Global.FilePathPrefix; - LoadTieLines(); + Debug.LogMessage(LogEventLevel.Information, "FilePathPrefix: {filePathPrefix:l}", configDir); + var configExists = Directory.Exists(configDir); + if (!configExists) + Directory.Create(configDir); - /*var mobileControl = GetMobileControlDevice(); + var irDir = Global.FilePathPrefix + "ir"; + if (!Directory.Exists(irDir)) + Directory.Create(irDir); + + var sgdDir = Global.FilePathPrefix + "sgd"; + if (!Directory.Exists(sgdDir)) + Directory.Create(sgdDir); + + var pluginDir = Global.FilePathPrefix + "plugins"; + if (!Directory.Exists(pluginDir)) + Directory.Create(pluginDir); + + var joinmapDir = Global.FilePathPrefix + "joinmaps"; + if (!Directory.Exists(joinmapDir)) + Directory.Create(joinmapDir); + + return configExists; + } + + /// + /// TearDown method + /// + public void TearDown() + { + Debug.LogMessage(LogEventLevel.Information, "Tearing down existing system"); + DeviceManager.DeactivateAll(); + + TieLineCollection.Default.Clear(); + + foreach (var key in DeviceManager.GetDevices()) + DeviceManager.RemoveDevice(key); + + Debug.LogMessage(LogEventLevel.Information, "Tear down COMPLETE"); + } + + + /// + /// Load method + /// + void Load() + { + LoadDevices(); + LoadRooms(); + LoadLogoServer(); + + DeviceManager.ActivateAll(); + + LoadTieLines(); + + /*var mobileControl = GetMobileControlDevice(); if (mobileControl == null) return; - mobileControl.LinkSystemMonitorToAppServer();*/ + mobileControl.LinkSystemMonitorToAppServer();*/ + } + + /// + /// Reads all devices from config and adds them to DeviceManager + /// + public void LoadDevices() + { + + // Build the processor wrapper class + DeviceManager.AddDevice(new Core.Devices.CrestronProcessor("processor")); + + DeviceManager.AddDevice(new RoutingFeedbackManager($"routingFeedbackManager", "Routing Feedback Manager")); + + // Add global System Monitor device + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) + { + DeviceManager.AddDevice( + new Core.Monitoring.SystemMonitorController("systemMonitor")); } - /// - /// LoadDevices method - /// - public void LoadDevices() + foreach (var devConf in ConfigReader.ConfigObject.Devices) { + IKeyed newDev = null; - // Build the processor wrapper class - DeviceManager.AddDevice(new Core.Devices.CrestronProcessor("processor")); - - DeviceManager.AddDevice(new RoutingFeedbackManager($"routingFeedbackManager", "Routing Feedback Manager")); - - // Add global System Monitor device - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) + try { - DeviceManager.AddDevice( - new Core.Monitoring.SystemMonitorController("systemMonitor")); - } - - foreach (var devConf in ConfigReader.ConfigObject.Devices) - { - IKeyed newDev = null; - - try + Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type); + // Skip this to prevent unnecessary warnings + if (devConf.Key == "processor") { - Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type); - // Skip this to prevent unnecessary warnings - if (devConf.Key == "processor") - { - var prompt = Global.ControlSystem.ControllerPrompt; + var prompt = Global.ControlSystem.ControllerPrompt; - var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) || - string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase); + var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) || + string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase); - if (!typeMatch) - Debug.LogMessage(LogEventLevel.Information, - "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", - devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); + if (!typeMatch) + Debug.LogMessage(LogEventLevel.Information, + "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", + devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); - continue; - } - - - if (newDev == null) - newDev = Core.DeviceFactory.GetDevice(devConf); - - if (newDev != null) - DeviceManager.AddDevice(newDev); - else - Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); - } - catch (Exception e) - { - Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); - } - } - Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded."); - - } - - - /// - /// LoadTieLines method - /// - public void LoadTieLines() - { - // In the future, we can't necessarily just clear here because devices - // might be making their own internal sources/tie lines - - var tlc = TieLineCollection.Default; - - if (ConfigReader.ConfigObject.TieLines == null) - { - return; - } - - foreach (var tieLineConfig in ConfigReader.ConfigObject.TieLines) - { - var newTL = tieLineConfig.GetTieLine(); - if (newTL != null) - tlc.Add(newTL); - } - - Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded."); - - } - - /// - /// LoadRooms method - /// - public void LoadRooms() - { - if (ConfigReader.ConfigObject.Rooms == null) - { - Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration."); - return; - } - - foreach (var roomConfig in ConfigReader.ConfigObject.Rooms) - { - try - { - var room = Core.DeviceFactory.GetDevice(roomConfig); - - if (room == null) - { - Debug.LogWarning("ERROR: Cannot load unknown room type '{roomType:l}', key '{roomKey:l}'.", roomConfig.Type, roomConfig.Key); - continue; - } - - DeviceManager.AddDevice(room); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Exception loading room {roomKey}:{roomType}", null, roomConfig.Key, roomConfig.Type); continue; } + + + if (newDev == null) + newDev = Core.DeviceFactory.GetDevice(devConf); + + if (newDev != null) + DeviceManager.AddDevice(newDev); + else + Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); } + catch (Exception e) + { + Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); + } + } + Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded."); - Debug.LogMessage(LogEventLevel.Information, "All Rooms Loaded."); + } + /// + /// Helper method to load tie lines. This should run after devices have loaded + /// + public void LoadTieLines() + { + // In the future, we can't necessarily just clear here because devices + // might be making their own internal sources/tie lines + + var tlc = TieLineCollection.Default; + + if (ConfigReader.ConfigObject.TieLines == null) + { + return; } - /// - /// Fires up a logo server if not already running - /// - void LoadLogoServer() + foreach (var tieLineConfig in ConfigReader.ConfigObject.TieLines) { - if (ConfigReader.ConfigObject.Rooms == null) - { - Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup."); - return; - } + var newTL = tieLineConfig.GetTieLine(); + if (newTL != null) + tlc.Add(newTL); + } - if ( - !ConfigReader.ConfigObject.Rooms.Any( - CheckRoomConfig)) - { - Debug.LogMessage(LogEventLevel.Information, "No rooms configured to use system Logo server. Bypassing Logo server startup"); - return; - } + Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded."); + } + + /// + /// Reads all rooms from config and adds them to DeviceManager + /// + public void LoadRooms() + { + if (ConfigReader.ConfigObject.Rooms == null) + { + Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration."); + return; + } + + foreach (var roomConfig in ConfigReader.ConfigObject.Rooms) + { try { - LogoServer = new HttpLogoServer(8080, Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo"); + var room = Core.DeviceFactory.GetDevice(roomConfig); + + if (room == null) + { + Debug.LogWarning("ERROR: Cannot load unknown room type '{roomType:l}', key '{roomKey:l}'.", roomConfig.Type, roomConfig.Key); + continue; + } + + DeviceManager.AddDevice(room); } - catch (Exception) + catch (Exception ex) { - Debug.LogMessage(LogEventLevel.Information, "NOTICE: Logo server cannot be started. Likely already running in another program"); + Debug.LogMessage(ex, "Exception loading room {roomKey}:{roomType}", null, roomConfig.Key, roomConfig.Type); + continue; } } - private bool CheckRoomConfig(DeviceConfig c) + Debug.LogMessage(LogEventLevel.Information, "All Rooms Loaded."); + + + } + + /// + /// Fires up a logo server if not already running + /// + void LoadLogoServer() + { + if (ConfigReader.ConfigObject.Rooms == null) { - string logoDark = null; - string logoLight = null; - string logo = null; + Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup."); + return; + } - try + if ( + !ConfigReader.ConfigObject.Rooms.Any( + CheckRoomConfig)) + { + Debug.LogMessage(LogEventLevel.Information, "No rooms configured to use system Logo server. Bypassing Logo server startup"); + return; + } + + try + { + LogoServer = new HttpLogoServer(8080, Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo"); + } + catch (Exception) + { + Debug.LogMessage(LogEventLevel.Information, "NOTICE: Logo server cannot be started. Likely already running in another program"); + } + } + + private bool CheckRoomConfig(DeviceConfig c) + { + string logoDark = null; + string logoLight = null; + string logo = null; + + try + { + if (c.Properties["logoDark"] != null) { - if (c.Properties["logoDark"] != null) - { - logoDark = c.Properties["logoDark"].Value("type"); - } - - if (c.Properties["logoLight"] != null) - { - logoLight = c.Properties["logoLight"].Value("type"); - } - - if (c.Properties["logo"] != null) - { - logo = c.Properties["logo"].Value("type"); - } - - return ((logoDark != null && logoDark == "system") || - (logoLight != null && logoLight == "system") || (logo != null && logo == "system")); + logoDark = c.Properties["logoDark"].Value("type"); } - catch + + if (c.Properties["logoLight"] != null) { - Debug.LogMessage(LogEventLevel.Information, "Unable to find logo information in any room config"); - return false; + logoLight = c.Properties["logoLight"].Value("type"); + } + + if (c.Properties["logo"] != null) + { + logo = c.Properties["logo"].Value("type"); + } + + return ((logoDark != null && logoDark == "system") || + (logoLight != null && logoLight == "system") || (logo != null && logo == "system")); + } + catch + { + Debug.LogMessage(LogEventLevel.Information, "Unable to find logo information in any room config"); + return false; + } + } + + private static void LoadAssets() + { + var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); + Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.FilePathPrefix); + + var zipFiles = applicationDirectory.GetFiles("assets*.zip"); + + if (zipFiles.Length > 1) + { + throw new Exception("Multiple assets zip files found. Cannot continue."); + } + + if (zipFiles.Length == 1) + { + var zipFile = zipFiles[0]; + var assetsRoot = System.IO.Path.GetFullPath(Global.FilePathPrefix); + if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + assetsRoot += Path.DirectorySeparatorChar; + } + Debug.LogMessage(LogEventLevel.Information, "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName); + using (var archive = ZipFile.OpenRead(zipFile.FullName)) + { + foreach (var entry in archive.Entries) + { + var destinationPath = Path.Combine(Global.FilePathPrefix, entry.FullName); + var fullDest = System.IO.Path.GetFullPath(destinationPath); + if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(destinationPath); + continue; + } + + // If a directory exists where a file should go, delete it + if (Directory.Exists(destinationPath)) + Directory.Delete(destinationPath, true); + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); + entry.ExtractToFile(destinationPath, true); + Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); + } } } - private static void LoadAssets() + // cleaning up zip files + foreach (var file in zipFiles) { - var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); - Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.FilePathPrefix); + File.Delete(file.FullName); + } - var zipFiles = applicationDirectory.GetFiles("assets*.zip"); + var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip"); - if (zipFiles.Length > 1) + if (htmlZipFiles.Length > 1) + { + throw new Exception("Multiple htmlassets zip files found in application directory. Please ensure only one htmlassets*.zip file is present and retry."); + } + + + if (htmlZipFiles.Length == 1) + { + var htmlZipFile = htmlZipFiles[0]; + var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + var userOrNvramDir = programDir.Parent; + var rootDir = userOrNvramDir?.Parent; + if (rootDir == null) { - throw new Exception("Multiple assets zip files found. Cannot continue."); + throw new Exception($"Unable to determine root directory for html extraction. Current path: {Global.FilePathPrefix}"); } - - if (zipFiles.Length == 1) + var htmlDir = Path.Combine(rootDir.FullName, "html"); + var htmlRoot = System.IO.Path.GetFullPath(htmlDir); + if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && + !htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) { - var zipFile = zipFiles[0]; - var assetsRoot = System.IO.Path.GetFullPath(Global.FilePathPrefix); - if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + htmlRoot += Path.DirectorySeparatorChar; + } + Debug.LogMessage(LogEventLevel.Information, "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName); + using (var archive = ZipFile.OpenRead(htmlZipFile.FullName)) + { + foreach (var entry in archive.Entries) { - assetsRoot += Path.DirectorySeparatorChar; - } - Debug.LogMessage(LogEventLevel.Information, "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName); - using (var archive = ZipFile.OpenRead(zipFile.FullName)) - { - foreach (var entry in archive.Entries) + var destinationPath = Path.Combine(htmlDir, entry.FullName); + var fullDest = System.IO.Path.GetFullPath(destinationPath); + if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + + if (string.IsNullOrEmpty(entry.Name)) { - var destinationPath = Path.Combine(Global.FilePathPrefix, entry.FullName); - var fullDest = System.IO.Path.GetFullPath(destinationPath); - if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); - - if (string.IsNullOrEmpty(entry.Name)) - { - Directory.CreateDirectory(destinationPath); - continue; - } - - // If a directory exists where a file should go, delete it - if (Directory.Exists(destinationPath)) - Directory.Delete(destinationPath, true); - - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - entry.ExtractToFile(destinationPath, true); - Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); + Directory.CreateDirectory(destinationPath); + continue; } + + // Only delete the file if it exists and is a file, not a directory + if (File.Exists(destinationPath)) + File.Delete(destinationPath); + + var parentDir = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(parentDir)) + Directory.CreateDirectory(parentDir); + + entry.ExtractToFile(destinationPath, true); + Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); } } + } - // cleaning up zip files - foreach (var file in zipFiles) + // cleaning up html zip files + foreach (var file in htmlZipFiles) + { + File.Delete(file.FullName); + } + + var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json"); + + if (jsonFiles.Length > 1) + { + Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", jsonFiles.Select(f => f.FullName).ToArray()); + throw new Exception("Multiple configuration files found. Cannot continue."); + } + + if (jsonFiles.Length == 1) + { + var jsonFile = jsonFiles[0]; + var finalPath = Path.Combine(Global.FilePathPrefix, jsonFile.Name); + Debug.LogMessage(LogEventLevel.Information, "Found configuration file: {jsonFile:l}... Moving to: {Destination}", jsonFile.FullName, finalPath); + + if (File.Exists(finalPath)) { - File.Delete(file.FullName); + Debug.LogMessage(LogEventLevel.Information, "Removing existing configuration file: {Destination}", finalPath); + File.Delete(finalPath); } - var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip"); - - if (htmlZipFiles.Length > 1) - { - throw new Exception("Multiple htmlassets zip files found in application directory. Please ensure only one htmlassets*.zip file is present and retry."); - } - - - if (htmlZipFiles.Length == 1) - { - var htmlZipFile = htmlZipFiles[0]; - var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); - var userOrNvramDir = programDir.Parent; - var rootDir = userOrNvramDir?.Parent; - if (rootDir == null) - { - throw new Exception($"Unable to determine root directory for html extraction. Current path: {Global.FilePathPrefix}"); - } - var htmlDir = Path.Combine(rootDir.FullName, "html"); - var htmlRoot = System.IO.Path.GetFullPath(htmlDir); - if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && - !htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) - { - htmlRoot += Path.DirectorySeparatorChar; - } - Debug.LogMessage(LogEventLevel.Information, "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName); - using (var archive = ZipFile.OpenRead(htmlZipFile.FullName)) - { - foreach (var entry in archive.Entries) - { - var destinationPath = Path.Combine(htmlDir, entry.FullName); - var fullDest = System.IO.Path.GetFullPath(destinationPath); - if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); - - if (string.IsNullOrEmpty(entry.Name)) - { - Directory.CreateDirectory(destinationPath); - continue; - } - - // Only delete the file if it exists and is a file, not a directory - if (File.Exists(destinationPath)) - File.Delete(destinationPath); - - var parentDir = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrEmpty(parentDir)) - Directory.CreateDirectory(parentDir); - - entry.ExtractToFile(destinationPath, true); - Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); - } - } - } - - // cleaning up html zip files - foreach (var file in htmlZipFiles) - { - File.Delete(file.FullName); - } - - var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json"); - - if (jsonFiles.Length > 1) - { - Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", jsonFiles.Select(f => f.FullName).ToArray()); - throw new Exception("Multiple configuration files found. Cannot continue."); - } - - if (jsonFiles.Length == 1) - { - var jsonFile = jsonFiles[0]; - var finalPath = Path.Combine(Global.FilePathPrefix, jsonFile.Name); - Debug.LogMessage(LogEventLevel.Information, "Found configuration file: {jsonFile:l}... Moving to: {Destination}", jsonFile.FullName, finalPath); - - if (File.Exists(finalPath)) - { - Debug.LogMessage(LogEventLevel.Information, "Removing existing configuration file: {Destination}", finalPath); - File.Delete(finalPath); - } - - jsonFile.MoveTo(finalPath); - } + jsonFile.MoveTo(finalPath); } } } + diff --git a/src/PepperDash.Essentials/Factory/DeviceFactory.cs b/src/PepperDash.Essentials/Factory/DeviceFactory.cs index 36fca96a..dcfaf26e 100644 --- a/src/PepperDash.Essentials/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials/Factory/DeviceFactory.cs @@ -7,58 +7,54 @@ using System.Reflection; using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +/// +/// Responsible for loading all of the device types for this library +/// +public class DeviceFactory { - /// - /// Responsible for loading all of the device types for this library - /// - public class DeviceFactory + + public DeviceFactory() { + var assy = Assembly.GetExecutingAssembly(); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); - /// - /// Initializes a new instance of the DeviceFactory class and loads all device type factories - /// - public DeviceFactory() + var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + + if (types != null) { - var assy = Assembly.GetExecutingAssembly(); - PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); - - var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); - - if (types != null) + foreach (var type in types) { - foreach (var type in types) + try { - try - { - var factory = (IDeviceFactory)Activator.CreateInstance(type); - LoadDeviceFactories(factory); - } - catch (Exception e) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Unable to load type: '{exception}' DeviceFactory: {factoryName}", e, type.Name); - } + var factory = (IDeviceFactory)Activator.CreateInstance(type); + LoadDeviceFactories(factory); + } + catch (Exception e) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Unable to load type: '{exception}' DeviceFactory: {factoryName}", e, type.Name); } } } + } - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) { - foreach (var typeName in deviceFactory.TypeNames) - { - string description = (deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description - : "No description available"; - - Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); - } + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } } diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index 856b881f..d122c7b0 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -27,29 +27,6 @@ pdbonly bin\$(Configuration)\PepperDashEssentials.xml - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - From 7f60dcb4cf8ea0e1094ba4ec747e14e813970d03 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 4 Jul 2025 16:23:02 -0500 Subject: [PATCH 016/161] 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 49e41a67..ef8e86b0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 2.19.4-local + 3.0.0-local $(Version) PepperDash Technology PepperDash Technology From 5d90fafbd730d5a2614847d65b50499d1c5263d9 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 7 Jul 2025 10:06:59 -0500 Subject: [PATCH 017/161] chore: remove duplication namespace declaration Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Plugins/PluginLoader.cs | 113 ++++++++---------- 1 file changed, 48 insertions(+), 65 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index deec08cb..c3259305 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -4,18 +4,15 @@ using System.Linq; using Crestron.SimplSharp; using System.Reflection; using System.IO; -using Newtonsoft.Json; using System.Reflection.PortableExecutable; using System.Reflection.Metadata; -using SystemIO = System.IO; -using CrestronIO = System.IO; using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; -namespace PepperDash.Essentials; +namespace PepperDash.Essentials; /// /// Provides functionality for loading and managing plugins and assemblies in the application. /// @@ -75,19 +72,6 @@ public static class PluginLoader /// private static string TempDirectory => PluginDirectory + Global.DirectorySeparator + "temp"; - /// - /// The directory to look in for .cplz plugin packages - /// - static string _pluginDirectory => Global.FilePathPrefix + "plugins"; - - /// - /// The directory where plugins will be moved to and loaded from - /// - static string _loadedPluginsDirectoryPath => _pluginDirectory + Global.DirectorySeparator + "loadedAssemblies"; - - // The temp directory where .cplz archives will be unzipped to - static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp"; - /// /// Represents a collection of fully qualified type names that are known to be incompatible with the current /// application or framework. @@ -96,8 +80,8 @@ public static class PluginLoader /// incompatible with the intended usage of the application. These types may represent security risks, unsupported /// features, or legacy APIs that should be avoided. private static readonly HashSet KnownIncompatibleTypes = - [ - "System.Net.ICertificatePolicy", + [ + "System.Net.ICertificatePolicy", "System.Security.Cryptography.SHA1CryptoServiceProvider", "System.Web.HttpUtility", "System.Configuration.ConfigurationManager", @@ -108,7 +92,7 @@ public static class PluginLoader "System.Security.SecurityManager", "System.Security.Permissions.FileIOPermission", "System.AppDomain.CreateDomain" - ]; + ]; /// /// Initializes static members of the class. @@ -142,7 +126,6 @@ public static class PluginLoader Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length); - foreach (var fi in assemblyFiles.Where(fi => fi.Name.Contains("Essentials") || fi.Name.Contains("PepperDash"))) { string version = string.Empty; @@ -190,7 +173,6 @@ public static class PluginLoader } } - /// /// Associates the specified assembly with the given name in the loaded assemblies collection. /// @@ -314,7 +296,7 @@ public static class PluginLoader { string fileName = Path.GetFileName(filePath); Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); - + var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy); IncompatiblePlugins.Add(incompatiblePlugin); return null; @@ -335,15 +317,15 @@ public static class PluginLoader } return null; } - catch (FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) + catch(FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded")) { // Get the assembly name from the file path string assemblyName = Path.GetFileNameWithoutExtension(filePath); - + // Try to find the already loaded assembly var existingAssembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.OrdinalIgnoreCase)); - + if (existingAssembly != null) { Debug.LogMessage(LogEventLevel.Information, "Assembly '{0}' is already loaded, using existing instance", assemblyName); @@ -352,19 +334,19 @@ public static class PluginLoader LoadedAssemblies.Add(loadedAssembly); return loadedAssembly; } - + Debug.LogMessage(LogEventLevel.Warning, "Assembly with same name already loaded but couldn't find it: {0}", filePath); return null; } - catch (Exception ex) + catch(Exception ex) { string fileName = Path.GetFileName(filePath); - + // Check if this might be a .NET Framework compatibility issue - if (ex.Message.Contains("Could not load type") || + if (ex.Message.Contains("Could not load type") || ex.Message.Contains("Unable to load one or more of the requested types")) { - Debug.LogMessage(LogEventLevel.Error, "Error loading assembly {0}: Likely .NET 8 compatibility issue: {1}", + Debug.LogMessage(LogEventLevel.Error, "Error loading assembly {0}: Likely .NET 8 compatibility issue: {1}", fileName, ex.Message); IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, ex.Message, requestedBy)); } @@ -454,12 +436,12 @@ public static class PluginLoader { if (plugin.TriggeredBy != "Direct load") { - CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine, + CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine, plugin.Name, plugin.Reason, plugin.TriggeredBy); } else { - CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine, + CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine, plugin.Name, plugin.Reason); } } @@ -626,29 +608,29 @@ public static class PluginLoader // First, check compatibility of all assemblies before loading any var assemblyCompatibility = new Dictionary References)>(); - + foreach (var pluginFile in pluginFiles) { string fileName = pluginFile.Name; assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName); } - + // Now load compatible assemblies and track incompatible ones foreach (var pluginFile in pluginFiles) { string fileName = pluginFile.Name; var (isCompatible, reason, _) = assemblyCompatibility[fileName]; - + if (!isCompatible) { Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason); IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, reason, null)); continue; } - + // Try to load the assembly var loadedAssembly = LoadAssembly(pluginFile.FullName, null); - + if (loadedAssembly != null) { LoadedPluginFolderAssemblies.Add(loadedAssembly); @@ -668,13 +650,13 @@ public static class PluginLoader private static void LoadCustomPluginTypes() { Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types..."); - + foreach (var loadedAssembly in LoadedPluginFolderAssemblies) { // Skip if assembly is null (can happen if we had loading issues) if (loadedAssembly == null || loadedAssembly.Assembly == null) continue; - + // iteratate this assembly's classes, looking for "LoadPlugin()" methods try { @@ -689,7 +671,7 @@ public static class PluginLoader { Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", loadedAssembly.Name, e.Message); - + // Check if any of the loader exceptions are due to missing assemblies foreach (var loaderEx in e.LoaderExceptions) { @@ -698,18 +680,18 @@ public static class PluginLoader string missingAssembly = fileNotFoundEx.FileName; if (!string.IsNullOrEmpty(missingAssembly)) { - Debug.LogMessage(LogEventLevel.Warning, "Assembly {0} requires missing dependency: {1}", + Debug.LogMessage(LogEventLevel.Warning, "Assembly {0} requires missing dependency: {1}", loadedAssembly.Name, missingAssembly); - + // Add to incompatible plugins with dependency information IncompatiblePlugins.Add(new IncompatiblePlugin( - Path.GetFileName(missingAssembly), - $"Missing dependency required by {loadedAssembly.Name}", + Path.GetFileName(missingAssembly), + $"Missing dependency required by {loadedAssembly.Name}", loadedAssembly.Name)); } } } - + Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); continue; } @@ -718,18 +700,19 @@ public static class PluginLoader Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}", loadedAssembly.Name, e.Message); Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace); - + // Add to incompatible plugins if this is likely a .NET 8 compatibility issue - if (e.Message.Contains("Could not load type") || + if (e.Message.Contains("Could not load type") || e.Message.Contains("Unable to load one or more of the requested types")) { - IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, - $"Type loading error: {e.Message}", + IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, + $"Type loading error: {e.Message}", null)); } - + continue; } + foreach (var type in types) { try @@ -759,26 +742,26 @@ public static class PluginLoader Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}", loadedAssembly.Name, e.Message); Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace); - + // Add to incompatible plugins if this is likely a .NET 8 compatibility issue - if (e.Message.Contains("Could not load type") || + if (e.Message.Contains("Could not load type") || e.Message.Contains("Unable to load one or more of the requested types")) { - IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, - $"Assembly loading error: {e.Message}", + IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name, + $"Assembly loading error: {e.Message}", null)); } - + continue; } } - + // Update incompatible plugins with dependency information var pluginDependencies = new Dictionary>(); // Populate pluginDependencies with relevant data // Example: pluginDependencies["PluginA"] = new List { "Dependency1", "Dependency2" }; UpdateIncompatiblePluginDependencies(pluginDependencies); - + // plugin dll will be loaded. Any classes in plugin should have a static constructor // that registers that class with the Core.DeviceFactory Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types."); @@ -800,15 +783,15 @@ public static class PluginLoader // If it already has a requestedBy, skip it if (incompatiblePlugin.TriggeredBy != "Direct load") continue; - + // Find plugins that depend on this incompatible plugin foreach (var plugin in pluginDependencies) { string pluginName = plugin.Key; List dependencies = plugin.Value; - + // If this plugin depends on the incompatible plugin - if (dependencies.Contains(incompatiblePlugin.Name) || + if (dependencies.Contains(incompatiblePlugin.Name) || dependencies.Any(d => d.StartsWith(incompatiblePlugin.Name + ","))) { incompatiblePlugin.UpdateTriggeringPlugin(pluginName); @@ -853,7 +836,7 @@ public static class PluginLoader LoadDeviceFactories(deviceFactory); - if (!EssentialsPluginAssemblies.Contains(loadedAssembly)) + if(!EssentialsPluginAssemblies.Contains(loadedAssembly)) EssentialsPluginAssemblies.Add(loadedAssembly); } @@ -908,7 +891,7 @@ public static class PluginLoader // Load the types from any custom plugin assemblies LoadCustomPluginTypes(); } - + // Report on incompatible plugins if (IncompatiblePlugins.Count > 0) { @@ -917,12 +900,12 @@ public static class PluginLoader { if (plugin.TriggeredBy != "Direct load") { - Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})", + Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})", plugin.Name, plugin.Reason, plugin.TriggeredBy); } else { - Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}", + Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}", plugin.Name, plugin.Reason); } } From 3feab2593da8ff15af4df76c92c657d23a972cd3 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 6 Aug 2025 08:55:08 -0500 Subject: [PATCH 018/161] fix: use correct line endings for verbatim strings --- src/PepperDash.Core/Logging/Debug.cs | 222 +++++++++--------- src/PepperDash.Core/PepperDash.Core.csproj | 5 - .../Devices/DeviceManager.cs | 6 +- .../Factory/DeviceFactory.cs | 59 +++-- 4 files changed, 142 insertions(+), 150 deletions(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index b3e7452a..a322de1c 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -1,8 +1,7 @@ -extern alias NewtonsoftJson; +extern alias NewtonsoftJson; using System; using System.Collections.Generic; -using System.Net; using System.Reflection; using System.Text.RegularExpressions; using Crestron.SimplSharp; @@ -101,17 +100,19 @@ public static class Debug /// /// Version for the currently loaded PepperDashCore dll /// - public static string PepperDashCoreVersion { get; private set; } + public static string PepperDashCoreVersion { get; private set; } private static CTimer _saveTimer; - /// - /// When true, the IncludedExcludedKeys dict will contain keys to include. - /// When false (default), IncludedExcludedKeys will contain keys to exclude. - /// - private static bool _excludeAllMode; + /// + /// When true, the IncludedExcludedKeys dict will contain keys to include. + /// When false (default), IncludedExcludedKeys will contain keys to exclude. + /// + private static bool _excludeAllMode; - private static readonly Dictionary IncludedExcludedKeys; + //static bool ExcludeNoKeyMessages; + + private static readonly Dictionary IncludedExcludedKeys; private static readonly LoggerConfiguration _defaultLoggerConfiguration; @@ -255,7 +256,7 @@ public static class Debug { try { - var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel); + var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel); if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) { @@ -263,14 +264,15 @@ public static class Debug return LogEventLevel.Information; } - if(logLevel < 0 || logLevel > 5) + if (logLevel < 0 || logLevel > 5) { CrestronConsole.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); return LogEventLevel.Information; } return (LogEventLevel)logLevel; - } catch (Exception ex) + } + catch (Exception ex) { CrestronConsole.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); return LogEventLevel.Information; @@ -282,7 +284,7 @@ public static class Debug var assembly = Assembly.GetExecutingAssembly(); var ver = assembly - .GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), false); + .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); if (ver != null && ver.Length > 0) { @@ -330,13 +332,13 @@ public static class Debug if (levelString.Trim() == "?") { CrestronConsole.ConsoleCommandResponse( - $@"Used to set the minimum level of debug messages to be printed to the console: -{_logLevels[0]} = 0 -{_logLevels[1]} = 1 -{_logLevels[2]} = 2 -{_logLevels[3]} = 3 -{_logLevels[4]} = 4 -{_logLevels[5]} = 5"); + "Used to set the minimum level of debug messages to be printed to the console:\r\n" + + $"{_logLevels[0]} = 0\r\n" + + $"{_logLevels[1]} = 1\r\n" + + $"{_logLevels[2]} = 2\r\n" + + $"{_logLevels[3]} = 3\r\n" + + $"{_logLevels[4]} = 4\r\n" + + $"{_logLevels[5]} = 5"); return; } @@ -346,25 +348,25 @@ public static class Debug return; } - if(int.TryParse(levelString, out var levelInt)) + if (int.TryParse(levelString, out var levelInt)) { - if(levelInt < 0 || levelInt > 5) + if (levelInt < 0 || levelInt > 5) { CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5"); return; } - SetDebugLevel((uint) levelInt); + SetDebugLevel((uint)levelInt); return; } - if(Enum.TryParse(levelString, out var levelEnum)) + if (Enum.TryParse(levelString, out var levelEnum)) { SetDebugLevel(levelEnum); return; } CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); - } + } catch { CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); @@ -377,7 +379,7 @@ public static class Debug /// Valid values 0-5 public static void SetDebugLevel(uint level) { - if(!_logLevels.TryGetValue(level, out var logLevel)) + if (!_logLevels.TryGetValue(level, out var logLevel)) { logLevel = LogEventLevel.Information; @@ -396,9 +398,9 @@ public static class Debug CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}\r\n", InitialParametersClass.ApplicationNumber, _consoleLoggingLevelSwitch.MinimumLevel); - CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int) level}"); + CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); - var err = CrestronDataStoreStatic.SetLocalIntValue(LevelStoreKey, (int) level); + var err = CrestronDataStoreStatic.SetLocalIntValue(LevelStoreKey, (int)level); CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}"); @@ -408,9 +410,9 @@ public static class Debug public static void SetWebSocketMinimumDebugLevel(LogEventLevel level) { - _websocketLoggingLevelSwitch.MinimumLevel = level; + _websocketLoggingLevelSwitch.MinimumLevel = level; - var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint) level); + var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint)level); if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {erro}", err); @@ -468,80 +470,80 @@ public static class Debug /// Callback for console command /// /// - public static void SetDebugFilterFromConsole(string items) - { - var str = items.Trim(); - if (str == "?") - { - CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " + - "+all: at beginning puts filter into 'default include' mode\r" + - " All keys that follow will be excluded from output.\r" + - "-all: at beginning puts filter into 'default exclude all' mode.\r" + - " All keys that follow will be the only keys that are shown\r" + - "+nokey: Enables messages with no key (default)\r" + - "-nokey: Disables messages with no key.\r" + - "(nokey settings are independent of all other settings)"); - return; - } - var keys = Regex.Split(str, @"\s*"); - foreach (var keyToken in keys) - { - var lkey = keyToken.ToLower(); - if (lkey == "+all") - { - IncludedExcludedKeys.Clear(); - _excludeAllMode = false; - } - else if (lkey == "-all") - { - IncludedExcludedKeys.Clear(); - _excludeAllMode = true; - } - //else if (lkey == "+nokey") - //{ - // ExcludeNoKeyMessages = false; - //} - //else if (lkey == "-nokey") - //{ - // ExcludeNoKeyMessages = true; - //} - else - { - string key; - if (lkey.StartsWith("-")) - { - key = lkey.Substring(1); - // if in exclude all mode, we need to remove this from the inclusions - if (_excludeAllMode) - { - if (IncludedExcludedKeys.ContainsKey(key)) - IncludedExcludedKeys.Remove(key); - } - // otherwise include all mode, add to the exclusions - else - { - IncludedExcludedKeys[key] = new object(); - } - } - else if (lkey.StartsWith("+")) - { - key = lkey.Substring(1); - // if in exclude all mode, we need to add this as inclusion - if (_excludeAllMode) - { + public static void SetDebugFilterFromConsole(string items) + { + var str = items.Trim(); + if (str == "?") + { + CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " + + "+all: at beginning puts filter into 'default include' mode\r" + + " All keys that follow will be excluded from output.\r" + + "-all: at beginning puts filter into 'default exclude all' mode.\r" + + " All keys that follow will be the only keys that are shown\r" + + "+nokey: Enables messages with no key (default)\r" + + "-nokey: Disables messages with no key.\r" + + "(nokey settings are independent of all other settings)"); + return; + } + var keys = Regex.Split(str, @"\s*"); + foreach (var keyToken in keys) + { + var lkey = keyToken.ToLower(); + if (lkey == "+all") + { + IncludedExcludedKeys.Clear(); + _excludeAllMode = false; + } + else if (lkey == "-all") + { + IncludedExcludedKeys.Clear(); + _excludeAllMode = true; + } + //else if (lkey == "+nokey") + //{ + // ExcludeNoKeyMessages = false; + //} + //else if (lkey == "-nokey") + //{ + // ExcludeNoKeyMessages = true; + //} + else + { + string key; + if (lkey.StartsWith("-")) + { + key = lkey.Substring(1); + // if in exclude all mode, we need to remove this from the inclusions + if (_excludeAllMode) + { + if (IncludedExcludedKeys.ContainsKey(key)) + IncludedExcludedKeys.Remove(key); + } + // otherwise include all mode, add to the exclusions + else + { + IncludedExcludedKeys[key] = new object(); + } + } + else if (lkey.StartsWith("+")) + { + key = lkey.Substring(1); + // if in exclude all mode, we need to add this as inclusion + if (_excludeAllMode) + { - IncludedExcludedKeys[key] = new object(); - } - // otherwise include all mode, remove this from exclusions - else - { - if (IncludedExcludedKeys.ContainsKey(key)) - IncludedExcludedKeys.Remove(key); - } + IncludedExcludedKeys[key] = new object(); + } + // otherwise include all mode, remove this from exclusions + else + { + if (IncludedExcludedKeys.ContainsKey(key)) + IncludedExcludedKeys.Remove(key); } } } } + } @@ -566,7 +568,7 @@ public static class Debug public static object GetDeviceDebugSettingsForKey(string deviceKey) { return _contexts.GetDebugSettingsForKey(deviceKey); - } + } /// /// Sets the flag to prevent application starting on next boot @@ -626,7 +628,7 @@ public static class Debug public static void LogMessage(LogEventLevel level, string message, params object[] args) { - _logger.Write(level, message, args); + _logger.Write(level, message, args); } public static void LogMessage(LogEventLevel level, Exception ex, string message, params object[] args) @@ -650,7 +652,7 @@ public static class Debug #region Explicit methods for logging levels public static void LogVerbose(IKeyed keyed, string message, params object[] args) { - using(LogContext.PushProperty("Key", keyed?.Key)) + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Verbose, message, args); } @@ -658,7 +660,7 @@ public static class Debug public static void LogVerbose(Exception ex, IKeyed keyed, string message, params object[] args) { - using(LogContext.PushProperty("Key", keyed?.Key)) + using (LogContext.PushProperty("Key", keyed?.Key)) { _logger.Write(LogEventLevel.Verbose, ex, message, args); } @@ -812,7 +814,7 @@ public static class Debug if (!_logLevels.ContainsKey(level)) return; var logLevel = _logLevels[level]; - + LogMessage(logLevel, format, items); } @@ -849,7 +851,7 @@ public static class Debug } /// - /// Logs to Console when at-level, and all messages to error log, including device key + /// Logs to Console when at-level, and all messages to error log, including device key /// [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] public static void Console(uint level, IKeyed dev, string format, params object[] items) @@ -867,7 +869,7 @@ public static class Debug [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, string format, params object[] items) - { + { LogMessage(level, dev, format, items); } @@ -1005,7 +1007,7 @@ public static class Debug return string.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); } - return string.Format("{0}{1}user{1}debugSettings{1}{2}.json",Directory.GetApplicationRootDirectory(), Path.DirectorySeparatorChar, InitialParametersClass.RoomId); + return string.Format("{0}{1}user{1}debugSettings{1}{2}.json", Directory.GetApplicationRootDirectory(), Path.DirectorySeparatorChar, InitialParametersClass.RoomId); } /// @@ -1016,15 +1018,15 @@ public static class Debug /// /// Error /// - Error, + Error, /// /// Warning /// - Warning, + Warning, /// /// Notice /// - Notice, + Notice, /// /// None /// diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj index d35414f6..5d079434 100644 --- a/src/PepperDash.Core/PepperDash.Core.csproj +++ b/src/PepperDash.Core/PepperDash.Core.csproj @@ -55,11 +55,6 @@ - - - global,NewtonsoftJson - - diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index 915b10d0..dc9115d3 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -461,9 +461,9 @@ public static class DeviceManager if (String.IsNullOrEmpty(s) || s.Contains("?")) { CrestronConsole.ConsoleCommandResponse( - @"SETDEVICESTREAMDEBUG [{deviceKey}] [OFF |TX | RX | BOTH] [timeOutInMinutes] - {deviceKey} [OFF | TX | RX | BOTH] - Device to set stream debugging on, and which setting to use - timeOutInMinutes - Set timeout for stream debugging. Default is 30 minutes"); + "SETDEVICESTREAMDEBUG [{deviceKey}] [OFF |TX | RX | BOTH] [timeOutInMinutes]\r\n" + + " {deviceKey} [OFF | TX | RX | BOTH] - Device to set stream debugging on, and which setting to use\r\n" + + " timeOutInMinutes - Set timeout for stream debugging. Default is 30 minutes"); return; } diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs index 6726e763..e3591297 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Reflection; @@ -10,15 +11,9 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; -using System; -using System.Collections.Generic; -using System.Linq; -using System.ComponentModel.DataAnnotations; -using System.IO; namespace PepperDash.Essentials.Core; - /// /// Provides functionality for managing and registering device factories, including loading plugin-based factories and /// retrieving devices based on their configuration. @@ -39,14 +34,14 @@ public class DeviceFactory /// instantiated, an informational log message is generated, and the process continues with the remaining /// types. public DeviceFactory() - { + { var programAssemblies = Directory.GetFiles(InitialParametersClass.ProgramDirectory.ToString(), "*.dll"); - foreach(var assembly in programAssemblies) + foreach (var assembly in programAssemblies) { try { - Assembly.LoadFrom(assembly); + Assembly.LoadFrom(assembly); } catch (Exception e) { @@ -54,7 +49,7 @@ public class DeviceFactory } } - var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); // Loop through all loaded assemblies that contain at least 1 type that implements IDeviceFactory foreach (var assembly in loadedAssemblies) @@ -65,7 +60,7 @@ public class DeviceFactory var types = assembly.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); - if(types == null || !types.Any()) + if (types == null || !types.Any()) { Debug.LogDebug("No DeviceFactory types found in assembly: {assemblyName}", assembly.GetName().Name); continue; @@ -83,8 +78,8 @@ public class DeviceFactory Debug.LogError("Unable to load type: '{message}' DeviceFactory: {type}", e.Message, type.Name); } } - - } + + } } /// @@ -113,7 +108,7 @@ public class DeviceFactory private static readonly Dictionary FactoryMethods = new(StringComparer.OrdinalIgnoreCase); - /// + /// /// Registers a factory method for creating instances of a specific type. /// /// This method associates a type name with a factory method, allowing instances of the type to @@ -122,10 +117,10 @@ public class DeviceFactory /// The name of the type for which the factory method is being registered. This value cannot be null or empty. /// A delegate that defines the factory method. The delegate takes a parameter and /// returns an instance of . - public static void AddFactoryForType(string typeName, Func method) - { - FactoryMethods.Add(typeName, new DeviceFactoryWrapper() { FactoryMethod = method}); - } + public static void AddFactoryForType(string typeName, Func method) + { + FactoryMethods.Add(typeName, new DeviceFactoryWrapper() { FactoryMethod = method }); + } /// /// Registers a factory method for creating instances of a specific device type. @@ -139,7 +134,7 @@ public class DeviceFactory /// cref="IKeyed"/>. public static void AddFactoryForType(string typeName, string description, Type Type, Func method) { - if(FactoryMethods.ContainsKey(typeName)) + if (FactoryMethods.ContainsKey(typeName)) { Debug.LogInformation("Unable to add type: '{typeName}'. Already exists in DeviceFactory", typeName); return; @@ -157,7 +152,7 @@ public class DeviceFactory if (prop.Name.Equals("secret", StringComparison.CurrentCultureIgnoreCase)) { var secret = GetSecret(prop.Children().First().ToObject()); - + prop.Parent.Replace(secret); } if (prop.Value is not JObject recurseProp) return; @@ -170,7 +165,7 @@ public class DeviceFactory var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); if (secretProvider == null) return null; var secret = secretProvider.GetSecret(data.Key); - if (secret != null) return (string) secret.Value; + if (secret != null) return (string)secret.Value; Debug.LogMessage(LogEventLevel.Debug, "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", data.Provider, data.Key); @@ -191,7 +186,7 @@ public class DeviceFactory public static IKeyed GetDevice(DeviceConfig dc) { try - { + { var localDc = new DeviceConfig(dc); var key = localDc.Key; @@ -253,23 +248,23 @@ public class DeviceFactory } CrestronConsole.ConsoleCommandResponse( - @"Type: '{0}' - Type: '{1}' - Description: {2}{3}", type.Key, Type, description, CrestronEnvironment.NewLine); + "Type: '{0}'\r\n" + + " Type: '{1}'\r\n" + + " Description: {2}{3}", type.Key, Type, description, CrestronEnvironment.NewLine); } } - /// + /// /// Retrieves a dictionary of device factory wrappers, optionally filtered by a specified string. /// /// A string used to filter the dictionary keys. Only entries with keys containing the specified filter will be /// included. If or empty, all entries are returned. /// A dictionary where the keys are strings representing device identifiers and the values are instances. The dictionary may be empty if no entries match the filter. - public static Dictionary GetDeviceFactoryDictionary(string filter) - { - return string.IsNullOrEmpty(filter) - ? FactoryMethods - : FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value); - } + public static Dictionary GetDeviceFactoryDictionary(string filter) + { + return string.IsNullOrEmpty(filter) + ? FactoryMethods + : FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value); + } } From c2ff65dce3fcbae444abc76dc26b0ed10b480232 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 14 Aug 2025 08:55:32 -0500 Subject: [PATCH 019/161] build: remove unneeded references from PD Core project --- src/PepperDash.Core/PepperDash.Core.csproj | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj index 5d079434..6ff5dd71 100644 --- a/src/PepperDash.Core/PepperDash.Core.csproj +++ b/src/PepperDash.Core/PepperDash.Core.csproj @@ -35,12 +35,7 @@ - - - - - - + From bcb65c7fa4efa5fb19287624bd4ace7949b38616 Mon Sep 17 00:00:00 2001 From: Nick Genovese Date: Thu, 4 Sep 2025 15:42:31 -0400 Subject: [PATCH 020/161] ci(force-patch): force build From 426ef4ad6bcd1033ff9d0f42489e75f32b5701b6 Mon Sep 17 00:00:00 2001 From: Nick Genovese Date: Mon, 22 Sep 2025 10:17:16 -0400 Subject: [PATCH 021/161] fix: add error handling when saving debug settings --- src/PepperDash.Core/Logging/Debug.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index a322de1c..ae353abe 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -960,15 +960,24 @@ public static class Debug //if (!Directory.Exists(dir)) // Directory.Create(dir); - var fileName = GetMemoryFileName(); - - LogMessage(LogEventLevel.Information, "Loading debug settings file from {fileName}", fileName); - - using (var sw = new StreamWriter(fileName)) + try { - var json = JsonConvert.SerializeObject(_contexts); - sw.Write(json); - sw.Flush(); + var fileName = GetMemoryFileName(); + + LogMessage(LogEventLevel.Information, "Loading debug settings file from {fileName}", fileName); + + using (var sw = new StreamWriter(fileName)) + { + var json = JsonConvert.SerializeObject(_contexts); + sw.Write(json); + sw.Flush(); + } + } + catch (Exception ex) + { + ErrorLog.Error("Exception saving debug settings: {message}", ex); + CrestronConsole.PrintLine("Exception saving debug settings: {message}", ex.Message); + return; } } From 346a5e9e57a580094cf83630a020f5a14a168cab Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 10 Mar 2026 17:30:59 -0600 Subject: [PATCH 022/161] refactor: Refactor DeviceManager and related classes to improve thread safety and performance - Replaced CCriticalSection with lock statements in DeviceManager for better thread management. - Updated AddDevice and RemoveDevice methods to use Monitor for locking. - Enhanced event handling for device activation and registration. - Modified FileIO class to utilize Task for asynchronous file operations instead of CrestronInvoke. - Improved feedback mechanisms in FeedbackBase and SystemMonitorController using Task.Run. - Refactored GenericQueue to remove Crestron threading dependencies and utilize System.Threading. - Updated BlueJeansPc and VideoCodecBase classes to use Task for asynchronous operations. - Cleaned up unnecessary critical sections and improved code documentation across various files. --- .../Comm/CommunicationStreamDebugging.cs | 8 +- .../Comm/GenericSecureTcpIpClient.cs | 18 +- .../GenericSecureTcpIpClient_ForServer.cs | 18 +- .../Comm/GenericSecureTcpIpServer.cs | 129 +++--- src/PepperDash.Core/Comm/GenericSshClient.cs | 5 +- .../Comm/GenericTcpIpClient.cs | 433 +++++++++--------- .../Comm/GenericTcpIpServer.cs | 106 ++--- src/PepperDash.Core/Logging/DebugMemory.cs | 112 +++-- src/PepperDash.Core/Web/WebApiServer.cs | 387 +++++++--------- .../Config/Essentials/ConfigWriter.cs | 62 ++- .../Device Info/NetworkDeviceHelpers.cs | 82 ++-- .../Devices/DeviceManager.cs | 82 ++-- .../Feedbacks/FeedbackBase.cs | 5 +- src/PepperDash.Essentials.Core/File/FileIO.cs | 56 +-- .../Fusion/IEssentialsRoomFusionController.cs | 125 +++-- .../Monitoring/SystemMonitorController.cs | 5 +- .../Presets/DevicePresets.cs | 138 ++++-- .../Queues/GenericQueue.cs | 72 +-- .../SoftCodec/BlueJeansPc.cs | 36 +- .../VideoCodec/VideoCodecBase.cs | 3 +- .../Messengers/VideoCodecBaseMessenger.cs | 9 +- .../MobileControlSystemController.cs | 16 +- src/PepperDash.Essentials/ControlSystem.cs | 3 +- 23 files changed, 998 insertions(+), 912 deletions(-) diff --git a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs index 4739912a..d2da3257 100644 --- a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs +++ b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Crestron.SimplSharp; +using System.Timers; using PepperDash.Core; namespace PepperDash.Core; @@ -20,7 +20,7 @@ public class CommunicationStreamDebugging /// /// Timer to disable automatically if not manually disabled /// - private CTimer DebugExpiryPeriod; + private Timer DebugExpiryPeriod; /// /// The current debug setting @@ -93,7 +93,9 @@ public class CommunicationStreamDebugging StopDebugTimer(); - DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs); + DebugExpiryPeriod = new Timer(_DebugTimeoutInMs) { AutoReset = false }; + DebugExpiryPeriod.Elapsed += (s, e) => DisableDebugging(); + DebugExpiryPeriod.Start(); if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx) RxStreamDebuggingIsEnabled = true; diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs index 36e405b0..e81494e4 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -277,7 +279,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging /// /// Queue lock /// - CCriticalSection DequeueLock = new CCriticalSection(); + private readonly object _dequeueLock = new(); /// /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before @@ -696,9 +698,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. if (handler != null) { - var gotLock = DequeueLock.TryEnter(); - if (gotLock) - CrestronInvoke.BeginInvoke((o) => DequeueEvent()); + if (Monitor.TryEnter(_dequeueLock)) + Task.Run(() => DequeueEvent()); } } else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync @@ -708,7 +709,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging } /// - /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently. /// It will dequeue items as they are enqueued automatically. /// void DequeueEvent() @@ -730,11 +731,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging { this.LogError(e, "DequeueEvent error: {0}", e.Message); } - // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. - if (DequeueLock != null) - { - DequeueLock.Leave(); - } + // Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it. + Monitor.Exit(_dequeueLock); } void HeartbeatStart() diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs index d46ce374..42b35815 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs @@ -15,6 +15,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -271,7 +273,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect /// /// Queue lock /// - CCriticalSection DequeueLock = new CCriticalSection(); + private readonly object _dequeueLock = new(); /// /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before @@ -655,9 +657,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. if (handler != null) { - var gotLock = DequeueLock.TryEnter(); - if (gotLock) - CrestronInvoke.BeginInvoke((o) => DequeueEvent()); + if (Monitor.TryEnter(_dequeueLock)) + Task.Run(() => DequeueEvent()); } } else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync @@ -667,7 +668,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect } /// - /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently. /// It will dequeue items as they are enqueued automatically. /// void DequeueEvent() @@ -689,11 +690,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect { this.LogError("DequeueEvent error: {0}", e.Message, e); } - // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. - if (DequeueLock != null) - { - DequeueLock.Leave(); - } + // Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it. + Monitor.Exit(_dequeueLock); } void HeartbeatStart() diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs index 7fed3948..673afcbd 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs @@ -1,18 +1,9 @@ -/*PepperDash Technology Corp. -JAG -Copyright: 2017 ------------------------------------- -***Notice of Ownership and Copyright*** -The material in which this notice appears is the property of PepperDash Technology Corporation, -which claims copyright under the laws of the United States of America in the entire body of material -and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part, -of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited. -PepperDash Technology Corporation reserves all rights under applicable laws. ------------------------------------- */ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -69,12 +60,17 @@ public class GenericSecureTcpIpServer : Device /// /// Server listen lock /// - CCriticalSection ServerCCSection = new CCriticalSection(); + private readonly object _serverLock = new(); /// /// Queue lock /// - CCriticalSection DequeueLock = new CCriticalSection(); + private readonly object _dequeueLock = new(); + + /// + /// Broadcast lock + /// + private readonly object _broadcastLock = new(); /// /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before @@ -399,18 +395,19 @@ public class GenericSecureTcpIpServer : Device /// public void Listen() { - ServerCCSection.Enter(); + lock (_serverLock) + { try { if (Port < 1 || Port > 65535) { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); + this.LogError("Server '{0}': Invalid port", Key); ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); return; } if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); + this.LogError("Server '{0}': No Shared Key set", Key); ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); return; } @@ -434,22 +431,21 @@ public class GenericSecureTcpIpServer : Device SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error starting WaitForConnectionAsync {0}", status); + this.LogError("Error starting WaitForConnectionAsync {0}", status); } else { ServerStopped = false; } OnServerStateChange(SecureServer.State); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus); - ServerCCSection.Leave(); + this.LogInformation("Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus); } catch (Exception ex) { - ServerCCSection.Leave(); - ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key); + this.LogException(ex, "{1} Error with Dynamic Server: {0}", ex.ToString(), Key); } + } // end lock } /// @@ -459,18 +455,18 @@ public class GenericSecureTcpIpServer : Device { try { - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); + this.LogVerbose("Stopping Listener"); if (SecureServer != null) { SecureServer.Stop(); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State); + this.LogVerbose("Server State: {0}", SecureServer.State); OnServerStateChange(SecureServer.State); } ServerStopped = true; } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); + this.LogException(ex, "Error stopping server. Error: {0}", ex.Message); } } @@ -483,11 +479,11 @@ public class GenericSecureTcpIpServer : Device try { SecureServer.Disconnect(client); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); + this.LogVerbose("Disconnected client index: {0}", client); } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); + this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message); } } /// @@ -495,7 +491,7 @@ public class GenericSecureTcpIpServer : Device /// public void DisconnectAllClientsForShutdown() { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); + this.LogInformation("Disconnecting All Clients"); if (SecureServer != null) { SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange; @@ -507,17 +503,17 @@ public class GenericSecureTcpIpServer : Device try { SecureServer.Disconnect(i); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); + this.LogInformation("Disconnected client index: {0}", i); } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); + this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message); } } - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); + this.LogInformation("Server Status: {0}", SecureServer.ServerSocketStatus); } - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); + this.LogInformation("Disconnected All Clients"); ConnectedClientsIndexes.Clear(); if (!ProgramIsStopping) @@ -535,8 +531,8 @@ public class GenericSecureTcpIpServer : Device /// public void BroadcastText(string text) { - CCriticalSection CCBroadcast = new CCriticalSection(); - CCBroadcast.Enter(); + lock (_broadcastLock) + { try { if (ConnectedClientsIndexes.Count > 0) @@ -552,13 +548,12 @@ public class GenericSecureTcpIpServer : Device } } } - CCBroadcast.Leave(); } catch (Exception ex) { - CCBroadcast.Leave(); - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message); + this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message); } + } // end lock } /// @@ -579,7 +574,7 @@ public class GenericSecureTcpIpServer : Device } catch (Exception ex) { - Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); } } @@ -603,7 +598,7 @@ public class GenericSecureTcpIpServer : Device CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } - Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); + this.LogDebug("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); // Return Heartbeat SendTextToClient(HeartbeatStringToMatch, clientIndex); return remainingText; @@ -618,13 +613,13 @@ public class GenericSecureTcpIpServer : Device CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } - Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); + this.LogInformation("Heartbeat Received: {0}, from client index: {1}", received, clientIndex); } } } catch (Exception ex) { - Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + this.LogException(ex, "Error checking heartbeat: {0}", ex.Message); } return received; } @@ -636,11 +631,11 @@ public class GenericSecureTcpIpServer : Device /// public string GetClientIPAddress(uint clientIndex) { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); + this.LogInformation("GetClientIPAddress Index: {0}", clientIndex); if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) { var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); + this.LogInformation("GetClientIPAddress IPAddreess: {0}", ipa); return ipa; } @@ -663,7 +658,7 @@ public class GenericSecureTcpIpServer : Device clientIndex = (uint)o; address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", + this.LogInformation("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) @@ -703,7 +698,7 @@ public class GenericSecureTcpIpServer : Device // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber); + this.LogInformation("SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber); if (ConnectedClientsIndexes.Contains(clientIndex)) ConnectedClientsIndexes.Remove(clientIndex); @@ -725,12 +720,12 @@ public class GenericSecureTcpIpServer : Device } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); + this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex.Message); } //Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state //after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag //is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event. - CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null); + Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex))); } #endregion @@ -745,7 +740,7 @@ public class GenericSecureTcpIpServer : Device { try { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", + this.LogInformation("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); if (clientIndex != 0) @@ -765,7 +760,7 @@ public class GenericSecureTcpIpServer : Device } byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + this.LogInformation("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); } else { @@ -784,19 +779,19 @@ public class GenericSecureTcpIpServer : Device } else { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); + this.LogError("Client attempt faulty."); } } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message); } // Rearm the listner SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Socket status connect callback status {0}", status); + this.LogError("Socket status connect callback status {0}", status); if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS) { // There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact. @@ -833,7 +828,7 @@ public class GenericSecureTcpIpServer : Device if (received != SharedKey) { byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); + this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); mySecureTCPServer.SendData(clientIndex, b, b.Length); mySecureTCPServer.Disconnect(clientIndex); @@ -844,7 +839,7 @@ public class GenericSecureTcpIpServer : Device byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); OnServerClientReadyForCommunications(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); + this.LogInformation("Client with index {0} provided the shared key and successfully connected to the server", clientIndex); } else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) { @@ -857,7 +852,7 @@ public class GenericSecureTcpIpServer : Device } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); + this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex.Message); } if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); @@ -865,9 +860,8 @@ public class GenericSecureTcpIpServer : Device //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. if (handler != null) { - var gotLock = DequeueLock.TryEnter(); - if (gotLock) - CrestronInvoke.BeginInvoke((o) => DequeueEvent()); + if (Monitor.TryEnter(_dequeueLock)) + Task.Run(() => DequeueEvent()); } } else @@ -877,7 +871,7 @@ public class GenericSecureTcpIpServer : Device } /// - /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently. /// It will dequeue items as they are enqueued automatically. /// void DequeueEvent() @@ -899,11 +893,8 @@ public class GenericSecureTcpIpServer : Device { this.LogError(e, "DequeueEvent error"); } - // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. - if (DequeueLock != null) - { - DequeueLock.Leave(); - } + // Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it. + Monitor.Exit(_dequeueLock); } #endregion @@ -974,7 +965,7 @@ public class GenericSecureTcpIpServer : Device if (MonitorClient != null) MonitorClient.Disconnect(); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); + this.LogInformation("Program stopping. Closing server"); KillServer(); } } @@ -1015,7 +1006,7 @@ public class GenericSecureTcpIpServer : Device //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); + this.LogInformation("Starting monitor check"); MonitorClient.Connect(); // From here MonitorCLient either connects or hangs, MonitorClient will call back @@ -1042,7 +1033,7 @@ public class GenericSecureTcpIpServer : Device { if (args.IsReady) { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); + this.LogInformation("Monitor client connection success. Disconnecting in 2s"); MonitorClientTimer.Stop(); MonitorClientTimer = null; MonitorClientFailureCount = 0; @@ -1063,13 +1054,13 @@ public class GenericSecureTcpIpServer : Device StopMonitorClient(); if (MonitorClientFailureCount < MonitorClientMaxFailureCount) { - Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", + this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}", MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); StartMonitorClient(); } else { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, + this.LogError( "\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************", MonitorClientMaxFailureCount); diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index edc89a84..0bef3f74 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Threading; +using System.Threading.Tasks; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using Org.BouncyCastle.Utilities; @@ -135,8 +136,6 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR CTimer ReconnectTimer; - //Lock object to prevent simulatneous connect/disconnect operations - //private CCriticalSection connectLock = new CCriticalSection(); private SemaphoreSlim connectLock = new SemaphoreSlim(1); private bool DisconnectLogged = false; @@ -435,7 +434,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR /// void Client_ErrorOccurred(object sender, ExceptionEventArgs e) { - CrestronInvoke.BeginInvoke(o => + Task.Run(() => { if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) this.LogError("Disconnected by remote"); diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs index e880b386..52670899 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs @@ -4,17 +4,21 @@ using System; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using Timer = System.Timers.Timer; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; using Required = NewtonsoftJson::Newtonsoft.Json.Required; +using PepperDash.Core.Logging; +using System.Threading.Tasks; namespace PepperDash.Core; /// /// A class to handle basic TCP/IP communications with a server /// - public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect +public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect { private const string SplusKey = "Uninitialized TcpIpClient"; /// @@ -22,44 +26,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 + set + { + _hostname = value; + if (_client != null) { - _hostname = value; - if (_client != null) - { - _client.AddressClientConnectedTo = _hostname; - } + _client.AddressClientConnectedTo = _hostname; } } + } /// /// Port on server @@ -81,19 +85,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 /// @@ -102,15 +106,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; + } } /// @@ -122,26 +126,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 @@ -152,29 +156,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 readonly object _connectLock = new(); // private Timer for auto reconnect - private CTimer RetryTimer; + private Timer RetryTimer; /// /// Constructor @@ -183,9 +187,9 @@ namespace PepperDash.Core; /// /// /// - public GenericTcpIpClient(string key, string address, int port, int bufferSize) - : base(key) - { + public GenericTcpIpClient(string key, string address, int port, int bufferSize) + : base(key) + { StreamDebugging = new CommunicationStreamDebugging(key); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; @@ -193,10 +197,7 @@ namespace PepperDash.Core; Port = port; BufferSize = bufferSize; - RetryTimer = new CTimer(o => - { - Reconnect(); - }, Timeout.Infinite); + SetupRetryTimer(); } /// @@ -211,28 +212,30 @@ namespace PepperDash.Core; AutoReconnectIntervalMs = 5000; BufferSize = 2000; - RetryTimer = new CTimer(o => - { - Reconnect(); - }, Timeout.Infinite); + SetupRetryTimer(); } /// /// 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); - } + SetupRetryTimer(); + } + + private void SetupRetryTimer() + { + RetryTimer = new Timer { AutoReset = false, Enabled = false }; + RetryTimer.Elapsed += (s, e) => Reconnect(); + } + + /// /// Just to help S+ set the key @@ -249,7 +252,7 @@ namespace PepperDash.Core; { if (programEventType == eProgramStatusEventType.Stopping) { - Debug.Console(1, this, "Program stopping. Closing connection"); + this.LogInformation("Program stopping. Closing connection"); Deactivate(); } } @@ -258,42 +261,41 @@ namespace PepperDash.Core; /// /// /// - public override bool Deactivate() - { + 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; + } /// /// Attempts to connect to the server /// - public void Connect() - { + public void Connect() + { if (string.IsNullOrEmpty(Hostname)) { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); + this.LogWarning("GenericTcpIpClient '{0}': No address set", Key); return; } if (Port < 1 || Port > 65535) { { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key); + this.LogWarning("GenericTcpIpClient '{0}': Invalid port", Key); return; } } - try + lock (_connectLock) { - connectLock.Enter(); if (IsConnected) { - Debug.Console(1, this, "Connection already connected. Exiting Connect()"); + this.LogInformation("Connection already connected. Exiting Connect()"); } else { @@ -306,11 +308,7 @@ namespace PepperDash.Core; _client.ConnectToServerAsync(ConnectToServerCallback); } } - finally - { - connectLock.Leave(); - } - } + } private void Reconnect() { @@ -318,44 +316,34 @@ namespace PepperDash.Core; { return; } - try + lock (_connectLock) { - connectLock.Enter(); if (IsConnected || DisconnectCalledByUser == true) { - Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()"); + this.LogInformation("Reconnect no longer needed. Exiting Reconnect()"); } else { - Debug.Console(1, this, "Attempting reconnect now"); + this.LogInformation("Attempting reconnect now"); _client.ConnectToServerAsync(ConnectToServerCallback); } } - finally - { - connectLock.Leave(); - } } /// /// Attempts to disconnect the client /// - public void Disconnect() - { - try + public void Disconnect() + { + lock (_connectLock) { - connectLock.Enter(); DisconnectCalledByUser = true; // Stop trying reconnects, if we are RetryTimer.Stop(); DisconnectClient(); } - finally - { - connectLock.Leave(); - } - } + } /// /// Does the actual disconnect business @@ -364,7 +352,7 @@ namespace PepperDash.Core; { if (_client != null) { - Debug.Console(1, this, "Disconnecting client"); + this.LogInformation("Disconnecting client"); if (IsConnected) _client.DisconnectFromServer(); } @@ -374,50 +362,47 @@ namespace PepperDash.Core; /// Callback method for connection attempt /// /// - void ConnectToServerCallback(TCPClient c) - { + void ConnectToServerCallback(TCPClient c) + { if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { - Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); + this.LogInformation("Server connection result: {0}", c.ClientStatus); WaitAndTryReconnect(); } else { - Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); + this.LogInformation("Server connection result: {0}", c.ClientStatus); } - } + } /// /// Disconnects, waits and attemtps to connect again /// - void WaitAndTryReconnect() - { - CrestronInvoke.BeginInvoke(o => + void WaitAndTryReconnect() + { + Task.Run(() => { - try + lock (_connectLock) { - connectLock.Enter(); if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null) { DisconnectClient(); - Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus); - RetryTimer.Reset(AutoReconnectIntervalMs); + this.LogInformation("Attempting reconnect, status={0}", _client.ClientStatus); + RetryTimer.Stop(); + RetryTimer.Interval = AutoReconnectIntervalMs; + RetryTimer.Start(); } } - finally - { - connectLock.Leave(); - } }); - } + } /// /// Recieves incoming data /// /// /// - void Receive(TCPClient client, int numBytes) - { + void Receive(TCPClient client, int numBytes) + { if (client != null) { if (numBytes > 0) @@ -428,7 +413,7 @@ namespace PepperDash.Core; { if (StreamDebugging.RxStreamDebuggingIsEnabled) { - Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); + this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); } bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } @@ -439,135 +424,135 @@ namespace PepperDash.Core; if (StreamDebugging.RxStreamDebuggingIsEnabled) { - Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); + this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); } textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - } + } } client.ReceiveDataAsync(Receive); } - } + } - /// - /// General send method - /// - public void SendText(string text) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - // Check debug level before processing byte array + /// + /// General send 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)); + this.LogInformation("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 /// /// - public void SendBytes(byte[] bytes) - { + public void SendBytes(byte[] bytes) + { if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.LogInformation("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 /// /// /// - void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) - { + void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) + { if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { - Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); + this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); WaitAndTryReconnect(); } else { - Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); - _client.ReceiveDataAsync(Receive); + this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); + _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)); } +} /// /// Configuration properties for TCP/SSH Connections /// - public class TcpSshPropertiesConfig - { +public class TcpSshPropertiesConfig +{ /// /// Address to connect to /// - [JsonProperty(Required = Required.Always)] - public string Address { get; set; } - + [JsonProperty(Required = Required.Always)] + 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; } + public string Username { get; set; } /// /// Passord credential /// - public string Password { get; set; } + 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; } + /// + /// 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; + public TcpSshPropertiesConfig() + { + BufferSize = 32768; + AutoReconnect = true; + AutoReconnectIntervalMs = 5000; Username = ""; Password = ""; - } + } - } +} diff --git a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs index 3dce8895..41c19504 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -61,9 +62,14 @@ public class GenericTcpIpServer : Device #region Properties/Variables /// - /// + /// Server listen lock /// - CCriticalSection ServerCCSection = new CCriticalSection(); + object _serverLock = new(); + + /// + /// Broadcast lock + /// + private readonly object _broadcastLock = new(); /// @@ -365,12 +371,12 @@ public class GenericTcpIpServer : Device } else { - ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); + this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key); } } catch { - ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); + this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key); } } @@ -379,19 +385,18 @@ public class GenericTcpIpServer : Device /// public void Listen() { - ServerCCSection.Enter(); + lock (_serverLock) + { try { if (Port < 1 || Port > 65535) { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); - ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); + this.LogError("Server '{0}': Invalid port", Key); return; } if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); - ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); + this.LogError("Server '{0}': No Shared Key set", Key); return; } if (IsListening) @@ -417,18 +422,15 @@ public class GenericTcpIpServer : Device ServerStopped = false; myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); OnServerStateChange(myTcpServer.State); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus); + this.LogInformation("TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus); // StartMonitorClient(); - - - ServerCCSection.Leave(); } catch (Exception ex) { - ServerCCSection.Leave(); - ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key); + this.LogException(ex, "Error with Dynamic Server: {0}", ex.Message); } + } // end lock } /// @@ -438,18 +440,18 @@ public class GenericTcpIpServer : Device { try { - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); + this.LogDebug("Stopping Listener"); if (myTcpServer != null) { myTcpServer.Stop(); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State); + this.LogDebug("Server State: {0}", myTcpServer.State); OnServerStateChange(myTcpServer.State); } ServerStopped = true; } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); + this.LogException(ex, "Error stopping server. Error: {0}", ex.Message); } } @@ -462,11 +464,11 @@ public class GenericTcpIpServer : Device try { myTcpServer.Disconnect(client); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); + this.LogVerbose("Disconnected client index: {0}", client); } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); + this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message); } } /// @@ -474,7 +476,7 @@ public class GenericTcpIpServer : Device /// public void DisconnectAllClientsForShutdown() { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); + this.LogInformation("Disconnecting All Clients"); if (myTcpServer != null) { myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange; @@ -486,17 +488,17 @@ public class GenericTcpIpServer : Device try { myTcpServer.Disconnect(i); - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); + this.LogVerbose("Disconnected client index: {0}", i); } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); + this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message); } } - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus); + this.LogVerbose("Server Status: {0}", myTcpServer.ServerSocketStatus); } - Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); + this.LogInformation("Disconnected All Clients"); ConnectedClientsIndexes.Clear(); if (!ProgramIsStopping) @@ -514,8 +516,8 @@ public class GenericTcpIpServer : Device /// public void BroadcastText(string text) { - CCriticalSection CCBroadcast = new CCriticalSection(); - CCBroadcast.Enter(); + lock (_broadcastLock) + { try { if (ConnectedClientsIndexes.Count > 0) @@ -531,13 +533,12 @@ public class GenericTcpIpServer : Device } } } - CCBroadcast.Leave(); } catch (Exception ex) { - CCBroadcast.Leave(); - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message); + this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message); } + } // end lock } /// @@ -558,7 +559,7 @@ public class GenericTcpIpServer : Device } catch (Exception ex) { - Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); } } @@ -582,7 +583,7 @@ public class GenericTcpIpServer : Device CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } - Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); + this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); // Return Heartbeat SendTextToClient(HeartbeatStringToMatch, clientIndex); return remainingText; @@ -597,13 +598,13 @@ public class GenericTcpIpServer : Device CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); } - Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); + this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", received, clientIndex); } } } catch (Exception ex) { - Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + this.LogException(ex, "Error checking heartbeat: {0}", ex.Message); } return received; } @@ -615,11 +616,11 @@ public class GenericTcpIpServer : Device /// IP address of the client public string GetClientIPAddress(uint clientIndex) { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); + this.LogVerbose("GetClientIPAddress Index: {0}", clientIndex); if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) { var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); + this.LogVerbose("GetClientIPAddress IPAddreess: {0}", ipa); return ipa; } @@ -642,7 +643,7 @@ public class GenericTcpIpServer : Device clientIndex = (uint)o; address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", + this.LogWarning("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) @@ -678,7 +679,7 @@ public class GenericTcpIpServer : Device try { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + this.LogInformation("SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { if (ConnectedClientsIndexes.Contains(clientIndex)) @@ -697,7 +698,7 @@ public class GenericTcpIpServer : Device } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); + this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex); } onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); } @@ -714,7 +715,7 @@ public class GenericTcpIpServer : Device { try { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", + this.LogDebug("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); if (clientIndex != 0) @@ -734,10 +735,11 @@ public class GenericTcpIpServer : Device } byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); + this.LogDebug("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); } else { + this.LogDebug("Client at index {0} is ready for communications", clientIndex); OnServerClientReadyForCommunications(clientIndex); } if (HeartbeatRequired) @@ -753,7 +755,7 @@ public class GenericTcpIpServer : Device } else { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); + this.LogError("Client attempt faulty."); if (!ServerStopped) { server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); @@ -763,7 +765,7 @@ public class GenericTcpIpServer : Device } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex); } //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", // server.State, @@ -771,7 +773,7 @@ public class GenericTcpIpServer : Device // ServerStopped); if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection"); + this.LogDebug("Waiting for next connection"); server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); } @@ -802,7 +804,7 @@ public class GenericTcpIpServer : Device if (received != SharedKey) { byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); + this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); myTCPServer.SendData(clientIndex, b, b.Length); myTCPServer.Disconnect(clientIndex); return; @@ -812,7 +814,7 @@ public class GenericTcpIpServer : Device byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); myTCPServer.SendDataAsync(clientIndex, success, success.Length, null); OnServerClientReadyForCommunications(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); + this.LogDebug("Client with index {0} provided the shared key and successfully connected to the server", clientIndex); } else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) @@ -820,7 +822,7 @@ public class GenericTcpIpServer : Device } catch (Exception ex) { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); + this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex); } if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback); @@ -901,7 +903,7 @@ public class GenericTcpIpServer : Device if (MonitorClient != null) MonitorClient.Disconnect(); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); + this.LogInformation("Program stopping. Closing server"); KillServer(); } } @@ -942,7 +944,7 @@ public class GenericTcpIpServer : Device //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); + this.LogDebug("Starting monitor check"); MonitorClient.Connect(); // From here MonitorCLient either connects or hangs, MonitorClient will call back @@ -969,7 +971,7 @@ public class GenericTcpIpServer : Device { if (args.IsReady) { - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); + this.LogInformation("Monitor client connection success. Disconnecting in 2s"); MonitorClientTimer.Stop(); MonitorClientTimer = null; MonitorClientFailureCount = 0; @@ -990,13 +992,13 @@ public class GenericTcpIpServer : Device StopMonitorClient(); if (MonitorClientFailureCount < MonitorClientMaxFailureCount) { - Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", + this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}", MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); StartMonitorClient(); } else { - Debug.Console(2, this, Debug.ErrorLogLevel.Error, + this.LogError( "\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************", MonitorClientMaxFailureCount); diff --git a/src/PepperDash.Core/Logging/DebugMemory.cs b/src/PepperDash.Core/Logging/DebugMemory.cs index da327f77..ebd8c5b3 100644 --- a/src/PepperDash.Core/Logging/DebugMemory.cs +++ b/src/PepperDash.Core/Logging/DebugMemory.cs @@ -1,6 +1,7 @@ extern alias NewtonsoftJson; using System.Collections.Generic; +using System.Threading; using Crestron.SimplSharp; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute; @@ -9,62 +10,61 @@ namespace PepperDash.Core.Logging; /// /// Class to persist current Debug settings across program restarts /// - public class DebugContextCollection - { +public class DebugContextCollection +{ /// /// To prevent threading issues with the DeviceDebugSettings collection /// - private readonly CCriticalSection _deviceDebugSettingsLock; + private readonly object _deviceDebugSettingsLock = new(); - [JsonProperty("items")] private readonly Dictionary _items; + [JsonProperty("items")] + private readonly Dictionary _items = new Dictionary(); /// /// Collection of the debug settings for each device where the dictionary key is the device key /// [JsonProperty("deviceDebugSettings")] - private Dictionary DeviceDebugSettings { get; set; } + private Dictionary DeviceDebugSettings { get; set; } = new Dictionary(); - /// - /// Default constructor - /// - public DebugContextCollection() - { - _deviceDebugSettingsLock = new CCriticalSection(); - DeviceDebugSettings = new Dictionary(); - _items = new Dictionary(); - } + /// + /// Default constructor + /// + public DebugContextCollection() + { - /// - /// Sets the level of a given context item, and adds that item if it does not - /// exist - /// - /// - /// - /// - /// SetLevel method - /// - public void SetLevel(string contextKey, int level) - { - if (level < 0 || level > 2) - return; - GetOrCreateItem(contextKey).Level = level; - } + } - /// - /// Gets a level or creates it if not existing - /// - /// - /// - /// - /// GetOrCreateItem method - /// - public DebugContextItem GetOrCreateItem(string contextKey) - { - if (!_items.ContainsKey(contextKey)) - _items[contextKey] = new DebugContextItem { Level = 0 }; - return _items[contextKey]; - } + /// + /// Sets the level of a given context item, and adds that item if it does not + /// exist + /// + /// + /// + /// + /// SetLevel method + /// + public void SetLevel(string contextKey, int level) + { + if (level < 0 || level > 2) + return; + GetOrCreateItem(contextKey).Level = level; + } + + /// + /// Gets a level or creates it if not existing + /// + /// + /// + /// + /// GetOrCreateItem method + /// + public DebugContextItem GetOrCreateItem(string contextKey) + { + if (!_items.ContainsKey(contextKey)) + _items[contextKey] = new DebugContextItem { Level = 0 }; + return _items[contextKey]; + } /// @@ -75,10 +75,8 @@ namespace PepperDash.Core.Logging; /// public void SetDebugSettingsForKey(string deviceKey, object settings) { - try + lock (_deviceDebugSettingsLock) { - _deviceDebugSettingsLock.Enter(); - if (DeviceDebugSettings.ContainsKey(deviceKey)) { DeviceDebugSettings[deviceKey] = settings; @@ -86,10 +84,6 @@ namespace PepperDash.Core.Logging; else DeviceDebugSettings.Add(deviceKey, settings); } - finally - { - _deviceDebugSettingsLock.Leave(); - } } /// @@ -101,22 +95,22 @@ namespace PepperDash.Core.Logging; { return DeviceDebugSettings[deviceKey]; } - } +} - /// - /// Contains information about - /// - public class DebugContextItem - { +/// +/// Contains information about +/// +public class DebugContextItem +{ /// /// The level of debug messages to print /// - [JsonProperty("level")] - public int Level { get; set; } + [JsonProperty("level")] + public int Level { get; set; } /// /// Property to tell the program not to intitialize when it boots, if desired /// [JsonProperty("doNotLoadOnNextBoot")] public bool DoNotLoadOnNextBoot { get; set; } - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs index 4b7c1c30..099817c7 100644 --- a/src/PepperDash.Core/Web/WebApiServer.cs +++ b/src/PepperDash.Core/Web/WebApiServer.cs @@ -3,287 +3,250 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using PepperDash.Core.Web.RequestHandlers; +using PepperDash.Core.Logging; namespace PepperDash.Core.Web; +/// +/// Web API server +/// +public class WebApiServer : IKeyName +{ + private const string SplusKey = "Uninitialized Web API Server"; + private const string DefaultName = "Web API Server"; + private const string DefaultBasePath = "/api"; + + private readonly object _serverLock = new(); + private HttpCwsServer _server; + /// - /// Web API server + /// Gets or sets the Key /// - public class WebApiServer : IKeyName + public string Key { get; private set; } + + /// + /// Gets or sets the Name + /// + public string Name { get; private set; } + + /// + /// Gets or sets the BasePath + /// + public string BasePath { get; private set; } + + /// + /// Gets or sets the IsRegistered + /// + public bool IsRegistered { get; private set; } + + /// + /// Constructor for S+. Make sure to set necessary properties using init method + /// + public WebApiServer() + : this(SplusKey, DefaultName, null) { - private const string SplusKey = "Uninitialized Web API Server"; - private const string DefaultName = "Web API Server"; - private const string DefaultBasePath = "/api"; + } - private const uint DebugTrace = 0; - private const uint DebugInfo = 1; - private const uint DebugVerbose = 2; + /// + /// Constructor + /// + /// + /// + public WebApiServer(string key, string basePath) + : this(key, DefaultName, basePath) + { + } - private readonly CCriticalSection _serverLock = new CCriticalSection(); - private HttpCwsServer _server; + /// + /// Constructor + /// + /// + /// + /// + public WebApiServer(string key, string name, string basePath) + { + Key = key; + Name = string.IsNullOrEmpty(name) ? DefaultName : name; + BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; - /// - /// Gets or sets the Key - /// - public string Key { get; private set; } + if (_server == null) _server = new HttpCwsServer(BasePath); - /// - /// Gets or sets the Name - /// - public string Name { get; private set; } + _server.setProcessName(Key); + _server.HttpRequestHandler = new DefaultRequestHandler(); - /// - /// Gets or sets the BasePath - /// - public string BasePath { get; private set; } + CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; + CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler; + } - /// - /// Gets or sets the IsRegistered - /// - public bool IsRegistered { get; private set; } + /// + /// Program status event handler + /// + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType != eProgramStatusEventType.Stopping) return; - /// - /// Http request handler - /// - //public IHttpCwsHandler HttpRequestHandler - //{ - // get { return _server.HttpRequestHandler; } - // set - // { - // if (_server == null) return; - // _server.HttpRequestHandler = value; - // } - //} + this.LogInformation("Program stopping. stopping server"); - /// - /// Received request event handler - /// - //public event EventHandler ReceivedRequestEvent - //{ - // add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); } - // remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); } - //} + Stop(); + } - /// - /// Constructor for S+. Make sure to set necessary properties using init method - /// - public WebApiServer() - : this(SplusKey, DefaultName, null) + /// + /// Ethernet event handler + /// + /// + void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) + { + // Re-enable the server if the link comes back up and the status should be connected + if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered) { + this.LogInformation("Ethernet link up. Server is already registered."); + return; } - /// - /// Constructor - /// - /// - /// - public WebApiServer(string key, string basePath) - : this(key, DefaultName, basePath) + this.LogInformation("Ethernet link up. Starting server"); + + Start(); + } + + /// + /// Initialize method + /// + public void Initialize(string key, string basePath) + { + Key = key; + BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; + } + + /// + /// Adds a route to CWS + /// + public void AddRoute(HttpCwsRoute route) + { + if (route == null) { + this.LogWarning("Failed to add route, route parameter is null"); + return; } - /// - /// Constructor - /// - /// - /// - /// - public WebApiServer(string key, string name, string basePath) + _server.Routes.Add(route); + + } + + /// + /// Removes a route from CWS + /// + /// + /// + /// RemoveRoute method + /// + public void RemoveRoute(HttpCwsRoute route) + { + if (route == null) { - Key = key; - Name = string.IsNullOrEmpty(name) ? DefaultName : name; - BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; - - if (_server == null) _server = new HttpCwsServer(BasePath); - - _server.setProcessName(Key); - _server.HttpRequestHandler = new DefaultRequestHandler(); - - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler; + this.LogWarning("Failed to remove route, route parameter is null"); + return; } - /// - /// Program status event handler - /// - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType != eProgramStatusEventType.Stopping) return; + _server.Routes.Remove(route); + } - Debug.Console(DebugInfo, this, "Program stopping. stopping server"); + /// + /// GetRouteCollection method + /// + public HttpCwsRouteCollection GetRouteCollection() + { + return _server.Routes; + } - Stop(); - } - - /// - /// Ethernet event handler - /// - /// - void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) - { - // Re-enable the server if the link comes back up and the status should be connected - if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered) - { - Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered."); - return; - } - - Debug.Console(DebugInfo, this, "Ethernet link up. Starting server"); - - Start(); - } - - /// - /// Initialize method - /// - public void Initialize(string key, string basePath) - { - Key = key; - BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; - } - - /// - /// Adds a route to CWS - /// - public void AddRoute(HttpCwsRoute route) - { - if (route == null) - { - Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null"); - return; - } - - _server.Routes.Add(route); - - } - - /// - /// Removes a route from CWS - /// - /// - /// - /// RemoveRoute method - /// - public void RemoveRoute(HttpCwsRoute route) - { - if (route == null) - { - Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null"); - return; - } - - _server.Routes.Remove(route); - } - - /// - /// GetRouteCollection method - /// - public HttpCwsRouteCollection GetRouteCollection() - { - return _server.Routes; - } - - /// - /// Starts CWS instance - /// - public void Start() + /// + /// Starts CWS instance + /// + public void Start() + { + lock (_serverLock) { try { - _serverLock.Enter(); - if (_server == null) { - Debug.Console(DebugInfo, this, "Server is null, unable to start"); + this.LogDebug("Server is null, unable to start"); return; } if (IsRegistered) { - Debug.Console(DebugInfo, this, "Server has already been started"); + this.LogDebug("Server has already been started"); return; } IsRegistered = _server.Register(); - Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed"); + this.LogDebug("Starting server, registration {0}", IsRegistered ? "was successful" : "failed"); } catch (Exception ex) { - Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message); - Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace); - if (ex.InnerException != null) - Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException); + this.LogException(ex, "Start Exception Message: {0}", ex.Message); + this.LogVerbose("Start Exception StackTrace: {0}", ex.StackTrace); } - finally - { - _serverLock.Leave(); - } - } + } // end lock + } - /// - /// Stop method - /// - public void Stop() + /// + /// Stop method + /// + public void Stop() + { + lock (_serverLock) { try { - _serverLock.Enter(); - if (_server == null) { - Debug.Console(DebugInfo, this, "Server is null or has already been stopped"); + this.LogDebug("Server is null or has already been stopped"); return; } IsRegistered = _server.Unregister() == false; - Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful"); + this.LogDebug("Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful"); _server.Dispose(); _server = null; } catch (Exception ex) { - Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message); - Debug.Console(DebugVerbose, this, "Server Stop Exception StackTrace: {0}", ex.StackTrace); - if (ex.InnerException != null) - Debug.Console(DebugVerbose, this, "Server Stop Exception InnerException: {0}", ex.InnerException); + this.LogException(ex, "Server Stop Exception Message: {0}", ex.Message); } - finally - { - _serverLock.Leave(); - } - } + } // end lock + } - /// - /// Received request handler - /// - /// - /// This is here for development and testing - /// - /// - /// - public void ReceivedRequestEventHandler(object sender, HttpCwsRequestEventArgs args) + /// + /// Received request handler + /// + /// + /// This is here for development and testing + /// + /// + /// + public void ReceivedRequestEventHandler(object sender, HttpCwsRequestEventArgs args) + { + try { - try - { - var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented); - Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j); - } - catch (Exception ex) - { - Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message); - Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace); - if (ex.InnerException != null) - Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException); - } + var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented); + this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j); } - } \ No newline at end of file + catch (Exception ex) + { + this.LogException(ex, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message); + this.LogVerbose("ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigWriter.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigWriter.cs index a688b378..423e05be 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigWriter.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigWriter.cs @@ -1,10 +1,9 @@  using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using Crestron.SimplSharp; +using System.Threading; +using Timer = System.Timers.Timer; using Crestron.SimplSharp.CrestronIO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -18,12 +17,27 @@ namespace PepperDash.Essentials.Core.Config; /// public class ConfigWriter { + /// + /// The name of the subfolder where the config file will be written + /// public const string LocalConfigFolder = "LocalConfig"; - public const long WriteTimeout = 30000; + /// + /// The amount of time in milliseconds to wait after the last config update before writing the config file. This is to prevent multiple rapid updates from causing multiple file writes. + /// Default is 30 seconds. + /// + public const long WriteTimeoutInMs = 30000; - public static CTimer WriteTimer; - static CCriticalSection fileLock = new CCriticalSection(); + private static Timer WriteTimer; + static readonly object _fileLock = new(); + + + static ConfigWriter() + { + WriteTimer = new Timer(WriteTimeoutInMs); + WriteTimer.Elapsed += (s, e) => WriteConfigFile(null); + + } /// /// Updates the config properties of a device @@ -53,6 +67,9 @@ public class ConfigWriter return success; } + /// + /// Updates the config properties of a device + /// public static bool UpdateDeviceConfig(DeviceConfig config) { bool success = false; @@ -73,17 +90,20 @@ public class ConfigWriter return success; } + /// + /// Updates the config properties of a room + /// public static bool UpdateRoomConfig(DeviceConfig config) { bool success = false; - var roomConfigIndex = ConfigReader.ConfigObject.Rooms.FindIndex(d => d.Key.Equals(config.Key)); + var roomConfigIndex = ConfigReader.ConfigObject.Rooms.FindIndex(d => d.Key.Equals(config.Key)); - if (roomConfigIndex >= 0) + if (roomConfigIndex >= 0) { ConfigReader.ConfigObject.Rooms[roomConfigIndex] = config; - Debug.LogMessage(LogEventLevel.Debug, "Updated room of device: '{0}'", config.Key); + Debug.LogMessage(LogEventLevel.Debug, "Updated config of room: '{0}'", config.Key); success = true; } @@ -98,10 +118,9 @@ public class ConfigWriter /// static void ResetTimer() { - if (WriteTimer == null) - WriteTimer = new CTimer(WriteConfigFile, WriteTimeout); - - WriteTimer.Reset(WriteTimeout); + WriteTimer.Stop(); + WriteTimer.Interval = WriteTimeoutInMs; + WriteTimer.Start(); Debug.LogMessage(LogEventLevel.Debug, "Config File write timer has been reset."); } @@ -120,10 +139,10 @@ public class ConfigWriter } /// - /// Writes + /// Writes the specified configuration data to a file. /// - /// - /// + /// The path of the file to write to. + /// The configuration data to write. public static void WriteFile(string filePath, string configData) { if (WriteTimer != null) @@ -133,9 +152,11 @@ public class ConfigWriter Debug.LogMessage(LogEventLevel.Information, "Attempting to write config file: '{0}'", filePath); + var lockAcquired = false; try { - if (fileLock.TryEnter()) + lockAcquired = Monitor.TryEnter(_fileLock); + if (lockAcquired) { using (StreamWriter sw = new StreamWriter(filePath)) { @@ -154,11 +175,8 @@ public class ConfigWriter } finally { - if (fileLock != null && !fileLock.Disposed) - fileLock.Leave(); - + if (lockAcquired) + Monitor.Exit(_fileLock); } } - - } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Device Info/NetworkDeviceHelpers.cs b/src/PepperDash.Essentials.Core/Device Info/NetworkDeviceHelpers.cs index 14390754..34557159 100644 --- a/src/PepperDash.Essentials.Core/Device Info/NetworkDeviceHelpers.cs +++ b/src/PepperDash.Essentials.Core/Device Info/NetworkDeviceHelpers.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Linq; using PepperDash.Core; using Crestron.SimplSharp; -using PepperDash.Essentials.Core; using Serilog.Events; namespace PepperDash.Essentials.Core.DeviceInfo; +/// +/// Helper methods for network devices +/// public static class NetworkDeviceHelpers { /// @@ -24,7 +26,7 @@ public static class NetworkDeviceHelpers private static readonly char NewLineSplitter = CrestronEnvironment.NewLine.ToCharArray().First(); private static readonly string NewLine = CrestronEnvironment.NewLine; - private static readonly CCriticalSection Lock = new CCriticalSection(); + private static readonly object _lock = new(); /// /// Last resolved ARP table - it is recommended to refresh the arp before using this. @@ -37,46 +39,45 @@ public static class NetworkDeviceHelpers public static void RefreshArp() { var error = false; - try + lock (_lock) { - Lock.Enter(); - var consoleResponse = string.Empty; - if (!CrestronConsole.SendControlSystemCommand("showarptable", ref consoleResponse)) return; - if (string.IsNullOrEmpty(consoleResponse)) + try { + var consoleResponse = string.Empty; + if (!CrestronConsole.SendControlSystemCommand("showarptable", ref consoleResponse)) return; + if (string.IsNullOrEmpty(consoleResponse)) + { + error = true; + return; + } + ArpTable.Clear(); + + Debug.LogMessage(LogEventLevel.Verbose, "ConsoleResponse of 'showarptable' : {0}{1}", NewLine, consoleResponse); + + var myLines = + consoleResponse.Split(NewLineSplitter) + .ToList() + .Where(o => (o.Contains(':') && !o.Contains("Type", StringComparison.OrdinalIgnoreCase))) + .ToList(); + foreach (var line in myLines) + { + var item = line; + var seperator = item.Contains('\t') ? '\t' : ' '; + var dataPoints = item.Split(seperator); + if (dataPoints == null || dataPoints.Length < 2) continue; + var ipAddress = SanitizeIpAddress(dataPoints.First().TrimAll()); + var macAddress = dataPoints.Last(); + ArpTable.Add(new ArpEntry(ipAddress, macAddress)); + } + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Information, "Exception in \"RefreshArp\" : {0}", ex.Message); error = true; - return; } - ArpTable.Clear(); + } // end lock - Debug.LogMessage(LogEventLevel.Verbose, "ConsoleResponse of 'showarptable' : {0}{1}", NewLine, consoleResponse); - - var myLines = - consoleResponse.Split(NewLineSplitter) - .ToList() - .Where(o => (o.Contains(':') && !o.Contains("Type", StringComparison.OrdinalIgnoreCase))) - .ToList(); - foreach (var line in myLines) - { - var item = line; - var seperator = item.Contains('\t') ? '\t' : ' '; - var dataPoints = item.Split(seperator); - if (dataPoints == null || dataPoints.Length < 2) continue; - var ipAddress = SanitizeIpAddress(dataPoints.First().TrimAll()); - var macAddress = dataPoints.Last(); - ArpTable.Add(new ArpEntry(ipAddress, macAddress)); - } - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Information, "Exception in \"RefreshArp\" : {0}", ex.Message); - error = true; - } - finally - { - Lock.Leave(); - OnArpTableUpdated(new ArpTableEventArgs(ArpTable, error)); - } + OnArpTableUpdated(new ArpTableEventArgs(ArpTable, error)); } @@ -158,7 +159,14 @@ public static class NetworkDeviceHelpers /// public class ArpEntry { + /// + /// IP Address of the ARP entry + /// public readonly IPAddress IpAddress; + + /// + /// MAC Address of the ARP entry + /// public readonly string MacAddress; /// diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index dc9115d3..7c800347 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using Crestron.SimplSharp; using Crestron.SimplSharpPro; using PepperDash.Core; @@ -10,13 +11,27 @@ using Serilog.Events; namespace PepperDash.Essentials.Core; +/// +/// Manages devices in the system, including activation and console commands to interact with devices +/// public static class DeviceManager { + /// + /// Event raised when all devices have been activated + /// public static event EventHandler AllDevicesActivated; + + /// + /// Event raised when all devices have been registered + /// public static event EventHandler AllDevicesRegistered; + + /// + /// Event raised when all devices have been initialized + /// public static event EventHandler AllDevicesInitialized; - private static readonly CCriticalSection DeviceCriticalSection = new CCriticalSection(); + private static readonly object _deviceLock = new(); //public static List Devices { get { return _Devices; } } //static List _Devices = new List(); @@ -28,7 +43,10 @@ public static class DeviceManager /// public static List AllDevices => [.. Devices.Values]; - public static bool AddDeviceEnabled; + /// + /// Flag to indicate whether adding devices is currently allowed. This is set to false once ActivateAll is called to prevent changes to the device list after activation. + /// + public static bool AddDeviceEnabled { get; private set; } /// /// Initializes the control system by enabling device management and registering console commands. @@ -65,11 +83,10 @@ public static class DeviceManager /// public static void ActivateAll() { - try - { - OnAllDevicesRegistered(); + OnAllDevicesRegistered(); - DeviceCriticalSection.Enter(); + lock (_deviceLock) + { AddDeviceEnabled = false; // PreActivate all devices Debug.LogMessage(LogEventLevel.Information, "****PreActivation starting...****"); @@ -125,11 +142,7 @@ public static class DeviceManager Debug.LogMessage(LogEventLevel.Information, "****PostActivation complete****"); OnAllDevicesActivated(); - } - finally - { - DeviceCriticalSection.Leave(); - } + } // end lock } private static void DeviceManager_Initialized(object sender, EventArgs e) @@ -176,18 +189,13 @@ public static class DeviceManager /// public static void DeactivateAll() { - try + lock (_deviceLock) { - DeviceCriticalSection.Enter(); foreach (var d in Devices.Values.OfType()) { d.Deactivate(); } } - finally - { - DeviceCriticalSection.Leave(); - } } //static void ListMethods(string devKey) @@ -266,11 +274,16 @@ public static class DeviceManager // Debug.LogMessage(LogEventLevel.Information, "Not yet implemented. Stay tuned"); //} + /// + /// Adds a device to the manager + /// public static void AddDevice(IKeyed newDev) { + var lockAcquired = false; try { - if (!DeviceCriticalSection.TryEnter()) + lockAcquired = Monitor.TryEnter(_deviceLock); + if (!lockAcquired) { Debug.LogMessage(LogEventLevel.Information, "Currently unable to add devices to Device Manager. Please try again"); return; @@ -300,15 +313,22 @@ public static class DeviceManager } finally { - DeviceCriticalSection.Leave(); + if (lockAcquired) + Monitor.Exit(_deviceLock); } } + /// + /// Adds a list of devices to the manager + /// + /// public static void AddDevice(IEnumerable devicesToAdd) { + var lockAcquired = false; try { - if (!DeviceCriticalSection.TryEnter()) + lockAcquired = Monitor.TryEnter(_deviceLock); + if (!lockAcquired) { Debug.LogMessage(LogEventLevel.Information, "Currently unable to add devices to Device Manager. Please try again"); @@ -336,15 +356,19 @@ public static class DeviceManager } finally { - DeviceCriticalSection.Leave(); + if (lockAcquired) + Monitor.Exit(_deviceLock); } } + /// + /// Removes a device from the manager + /// + /// The device to remove public static void RemoveDevice(IKeyed newDev) { - try + lock (_deviceLock) { - DeviceCriticalSection.Enter(); if (newDev == null) return; if (Devices.ContainsKey(newDev.Key)) @@ -354,18 +378,22 @@ public static class DeviceManager else Debug.LogMessage(LogEventLevel.Information, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key); } - finally - { - DeviceCriticalSection.Leave(); - } } + /// + /// Returns a list of all device keys currently in the manager + /// + /// A list of device keys + /// This method provides a way to retrieve a list of all device keys currently registered in the Device Manager. It returns an enumerable collection of strings representing the keys of the devices, allowing for easy access and manipulation of the device list as needed. public static IEnumerable GetDeviceKeys() { //return _Devices.Select(d => d.Key).ToList(); return Devices.Keys; } + /// + /// Returns a list of all devices currently in the manager + /// A list of devices public static IEnumerable GetDevices() { //return _Devices.Select(d => d.Key).ToList(); diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs index e54fc864..c230bbe8 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Crestron.SimplSharp; using PepperDash.Core; @@ -77,11 +78,11 @@ namespace PepperDash.Essentials.Core public abstract void FireUpdate(); /// - /// Fires the update asynchronously within a CrestronInvoke + /// Fires the update asynchronously within a Task /// public void InvokeFireUpdate() { - CrestronInvoke.BeginInvoke(o => FireUpdate()); + Task.Run(() => FireUpdate()); } /// diff --git a/src/PepperDash.Essentials.Core/File/FileIO.cs b/src/PepperDash.Essentials.Core/File/FileIO.cs index 25a6d87c..7d1fe721 100644 --- a/src/PepperDash.Essentials.Core/File/FileIO.cs +++ b/src/PepperDash.Essentials.Core/File/FileIO.cs @@ -1,11 +1,10 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; +using System.Threading; +using System.Threading.Tasks; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using PepperDash.Core; -using Crestron.SimplSharpPro.CrestronThread; using Serilog.Events; namespace PepperDash.Essentials.Core @@ -16,7 +15,7 @@ namespace PepperDash.Essentials.Core public static class FileIO { - static CCriticalSection fileLock = new CCriticalSection(); + static readonly object _fileLock = new(); /// /// Delegate for GotFileEventHandler /// @@ -103,9 +102,11 @@ namespace PepperDash.Essentials.Core /// public static string ReadDataFromFile(FileInfo file) { + var lockAcquired = false; try { - if (fileLock.TryEnter()) + lockAcquired = Monitor.TryEnter(_fileLock); + if (lockAcquired) { DirectoryInfo dirInfo = new DirectoryInfo(file.DirectoryName); Debug.LogMessage(LogEventLevel.Verbose, "FileIO Getting Data {0}", file.FullName); @@ -128,7 +129,6 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Information, "FileIO Unable to enter FileLock"); return ""; } - } catch (Exception e) { @@ -137,9 +137,8 @@ namespace PepperDash.Essentials.Core } finally { - if (fileLock != null && !fileLock.Disposed) - fileLock.Leave(); - + if (lockAcquired) + Monitor.Exit(_fileLock); } } @@ -166,7 +165,7 @@ namespace PepperDash.Essentials.Core { try { - CrestronInvoke.BeginInvoke(o => _ReadDataFromFileASync(file)); + Task.Run(() => _ReadDataFromFileASync(file)); } catch (Exception e) { @@ -177,9 +176,11 @@ namespace PepperDash.Essentials.Core private static void _ReadDataFromFileASync(FileInfo file) { string data; + var lockAcquired = false; try { - if (fileLock.TryEnter()) + lockAcquired = Monitor.TryEnter(_fileLock); + if (lockAcquired) { DirectoryInfo dirInfo = new DirectoryInfo(file.Name); Debug.LogMessage(LogEventLevel.Verbose, "FileIO Getting Data {0}", file.FullName); @@ -212,13 +213,9 @@ namespace PepperDash.Essentials.Core } finally { - if (fileLock != null && !fileLock.Disposed) - fileLock.Leave(); - + if (lockAcquired) + Monitor.Exit(_fileLock); } - - - } /// @@ -228,35 +225,35 @@ namespace PepperDash.Essentials.Core /// public static void WriteDataToFile(string data, string filePath) { - Thread _WriteFileThread; - _WriteFileThread = new Thread((O) => _WriteFileMethod(data, Global.FilePathPrefix + "/" + filePath), null, Thread.eThreadStartOptions.CreateSuspended); - _WriteFileThread.Priority = Thread.eThreadPriority.LowestPriority; + var _WriteFileThread = new System.Threading.Thread(() => _WriteFileMethod(data, Global.FilePathPrefix + "/" + filePath)) + { + IsBackground = true, + Priority = ThreadPriority.Lowest + }; _WriteFileThread.Start(); Debug.LogMessage(LogEventLevel.Information, "New WriteFile Thread"); - } - static object _WriteFileMethod(string data, string filePath) + static void _WriteFileMethod(string data, string filePath) { Debug.LogMessage(LogEventLevel.Information, "Attempting to write file: '{0}'", filePath); + var lockAcquired = false; try { - if (fileLock.TryEnter()) + lockAcquired = Monitor.TryEnter(_fileLock); + if (lockAcquired) { - using (StreamWriter sw = new StreamWriter(filePath)) { sw.Write(data); sw.Flush(); } - } else { Debug.LogMessage(LogEventLevel.Information, "FileIO Unable to enter FileLock"); } - } catch (Exception e) { @@ -264,12 +261,9 @@ namespace PepperDash.Essentials.Core } finally { - if (fileLock != null && !fileLock.Disposed) - fileLock.Leave(); - + if (lockAcquired) + Monitor.Exit(_fileLock); } - return null; - } /// diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index 9364a58d..564fa395 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using System.Timers; +using Timer = System.Timers.Timer; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronXml; @@ -78,6 +78,8 @@ namespace PepperDash.Essentials.Core.Fusion private bool _helpRequestSent; + private readonly object _guidFileLock = new(); + private eFusionHelpResponse _helpRequestStatus; /// @@ -390,44 +392,31 @@ namespace PepperDash.Essentials.Core.Fusion return; } - var fileLock = new CCriticalSection(); - - try + lock (_guidFileLock) { - if (fileLock.Disposed) + try { - return; + Debug.LogMessage(LogEventLevel.Debug, this, "Writing GUIDs to file"); + + _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); + + using (var sw = new StreamWriter(filePath)) + { + sw.Write(json); + sw.Flush(); + } + + Debug.LogMessage(LogEventLevel.Debug, this, "Guids successfully written to file '{0}'", filePath); } - - fileLock.Enter(); - - Debug.LogMessage(LogEventLevel.Debug, this, "Writing GUIDs to file"); - - _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); - - using (var sw = new StreamWriter(filePath)) + catch (Exception e) { - sw.Write(json); - sw.Flush(); + Debug.LogMessage(LogEventLevel.Information, this, "Error writing guid file: {0}", e); } - - Debug.LogMessage(LogEventLevel.Debug, this, "Guids successfully written to file '{0}'", filePath); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, this, "Error writing guid file: {0}", e); - } - finally - { - if (!fileLock.Disposed) - { - fileLock.Leave(); - } - } + } // end lock } /// @@ -442,50 +431,37 @@ namespace PepperDash.Essentials.Core.Fusion return; } - var fileLock = new CCriticalSection(); - - try + lock (_guidFileLock) { - if (fileLock.Disposed) + try { - return; + if (File.Exists(filePath)) + { + var json = File.ReadToEnd(filePath, Encoding.ASCII); + + _guids = JsonConvert.DeserializeObject(json); + + // _config.IpId = _guids.IpId; + + 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, _config.IpIdInt, RoomGuid); + + foreach (var item in FusionStaticAssets) + { + Debug.LogMessage(LogEventLevel.Debug, this, "\nAsset Name: {0}\nAsset No: {1}\n Guid: {2}", item.Value.Name, + item.Value.SlotNumber, item.Value.InstanceId); + } } - - fileLock.Enter(); - - if (File.Exists(filePath)) + catch (Exception e) { - var json = File.ReadToEnd(filePath, Encoding.ASCII); - - _guids = JsonConvert.DeserializeObject(json); - - // _config.IpId = _guids.IpId; - - FusionStaticAssets = _guids.StaticAssets; + Debug.LogMessage(LogEventLevel.Information, this, "Error reading guid file: {0}", e); } - - 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, _config.IpIdInt, RoomGuid); - - foreach (var item in FusionStaticAssets) - { - Debug.LogMessage(LogEventLevel.Debug, this, "\nAsset Name: {0}\nAsset No: {1}\n Guid: {2}", item.Value.Name, - item.Value.SlotNumber, item.Value.InstanceId); - } - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, this, "Error reading guid file: {0}", e); - } - finally - { - if (!fileLock.Disposed) - { - fileLock.Leave(); - } - } + } // end lock } /// @@ -1950,12 +1926,13 @@ namespace PepperDash.Essentials.Core.Fusion HelpRequestStatusFeedback.FireUpdate(); } - private void OnTimedEvent(object source, ElapsedEventArgs e) + private void OnTimedEvent(object sender, System.Timers.ElapsedEventArgs e) { this.LogInformation("Help request timeout reached for room '{0}'. Cancelling help request.", Room.Name); CancelHelpRequest(); } + /// public void CancelHelpRequest() { diff --git a/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs b/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs index d09ec98f..875dfd9b 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Essentials.Core.Bridges; using Serilog.Events; +using System.Threading.Tasks; namespace PepperDash.Essentials.Core.Monitoring; @@ -272,7 +273,7 @@ public class SystemMonitorController : EssentialsBridgeableDevice private void RefreshSystemMonitorData() { // this takes a while, launch a new thread - CrestronInvoke.BeginInvoke(UpdateFeedback); + Task.Run(() => UpdateFeedback(null)); } private void UpdateFeedback(object o) @@ -744,7 +745,7 @@ public class ProgramStatusFeedbacks /// public void GetProgramInfo() { - CrestronInvoke.BeginInvoke(GetProgramInfo); + Task.Run(() => GetProgramInfo(null)); } private void GetProgramInfo(object o) diff --git a/src/PepperDash.Essentials.Core/Presets/DevicePresets.cs b/src/PepperDash.Essentials.Core/Presets/DevicePresets.cs index 7346981b..4b009e45 100644 --- a/src/PepperDash.Essentials.Core/Presets/DevicePresets.cs +++ b/src/PepperDash.Essentials.Core/Presets/DevicePresets.cs @@ -1,15 +1,14 @@ - + using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Newtonsoft.Json; using PepperDash.Core; -//using SSMono.IO; -using PepperDash.Core.WebApi.Presets; using Serilog.Events; namespace PepperDash.Essentials.Core.Presets; @@ -19,11 +18,19 @@ namespace PepperDash.Essentials.Core.Presets; /// public class DevicePresetsModel : Device { + /// + /// Delegate for PresetRecalled event, which is fired when a preset is recalled. Provides the device and channel that was recalled. + /// + /// + /// public delegate void PresetRecalledCallback(ISetTopBoxNumericKeypad device, string channel); + /// + /// Delegate for PresetsSaved event, which is fired when presets are saved. Provides the list of presets that were saved. + /// public delegate void PresetsSavedCallback(List presets); - private readonly CCriticalSection _fileOps = new CCriticalSection(); + private readonly object _fileOps = new(); private readonly bool _initSuccess; private readonly ISetTopBoxNumericKeypad _setTopBox; @@ -37,6 +44,12 @@ public class DevicePresetsModel : Device private Action _enterFunction; private string _filePath; + /// + /// Constructor for DevicePresetsModel when a set top box device is included. If the set top box does not implement the required INumericKeypad interface, the model will still be created but dialing functionality will be disabled and a message will be logged. + /// + /// + /// + /// public DevicePresetsModel(string key, ISetTopBoxNumericKeypad setTopBox, string fileName) : this(key, fileName) { @@ -71,6 +84,11 @@ public class DevicePresetsModel : Device _enterFunction = setTopBox.KeypadEnter; } + /// + /// Constructor for DevicePresetsModel when only a file name is provided. Dialing functionality will be disabled. + /// + /// + /// public DevicePresetsModel(string key, string fileName) : base(key) { PulseTime = 150; @@ -88,27 +106,73 @@ public class DevicePresetsModel : Device _initSuccess = true; } + /// + /// Event fired when a preset is recalled, providing the device and channel that was recalled + /// public event PresetRecalledCallback PresetRecalled; + + /// + /// Event fired when presets are saved, providing the list of presets that were saved + /// public event PresetsSavedCallback PresetsSaved; - public int PulseTime { get; set; } - public int DigitSpacingMs { get; set; } + /// + /// Time in milliseconds to pulse the digit for when dialing a channel + /// + public int PulseTime { get; private set; } + + /// + /// Time in milliseconds to wait between pulsing digits when dialing a channel + /// + public int DigitSpacingMs { get; private set; } + + /// + /// Whether the presets have finished loading from the file or not + /// public bool PresetsAreLoaded { get; private set; } + /// + /// The list of presets to display + /// public List PresetsList { get; private set; } + /// + /// The number of presets in the list + /// public int Count { get { return PresetsList != null ? PresetsList.Count : 0; } } - public bool UseLocalImageStorage { get; set; } - public string ImagesLocalHostPrefix { get; set; } - public string ImagesPathPrefix { get; set; } - public string ListPathPrefix { get; set; } + /// + /// Indicates whether to use local image storage for preset images, which allows for more and larger images than the SIMPL+ zip file method + /// + public bool UseLocalImageStorage { get; private set; } + + /// + /// The prefix for the local host URL for preset images + /// + public string ImagesLocalHostPrefix { get; private set; } + + /// + /// The path prefix for preset images + /// + public string ImagesPathPrefix { get; private set; } + + /// + /// The path prefix for preset lists + /// + public string ListPathPrefix { get; private set; } + + /// + /// Event fired when presets are loaded + /// public event EventHandler PresetsLoaded; - + /// + /// Sets the file name for the presets list and loads the presets from that file. The file should be a JSON file in the format of the PresetsList class. If the file cannot be read, an empty list will be created and a message will be logged. This method is thread safe. + /// + /// The path to the presets file. public void SetFileName(string path) { _filePath = ListPathPrefix + path; @@ -117,12 +181,13 @@ public class DevicePresetsModel : Device LoadChannels(); } + /// + /// Loads the presets from the file specified by _filePath. + /// public void LoadChannels() { - try + lock (_fileOps) { - _fileOps.Enter(); - Debug.LogMessage(LogEventLevel.Verbose, this, "Loading presets from {0}", _filePath); PresetsAreLoaded = false; try @@ -149,12 +214,12 @@ public class DevicePresetsModel : Device handler(this, EventArgs.Empty); } } - finally - { - _fileOps.Leave(); - } } + /// + /// Dials a preset by its number in the list (starting at 1). If the preset number is out of range, nothing will happen. + /// + /// The number of the preset to dial, starting at 1 public void Dial(int presetNum) { if (presetNum <= PresetsList.Count) @@ -163,6 +228,10 @@ public class DevicePresetsModel : Device } } + /// + /// Dials a preset by its channel number. If the channel number contains characters that are not 0-9 or '-', those characters will be ignored. + /// If the model was not initialized with a valid set top box device, dialing will be disabled and a message will be logged. + /// The channel number to dial public void Dial(string chanNum) { if (_dialIsRunning || !_initSuccess) @@ -176,7 +245,7 @@ public class DevicePresetsModel : Device } _dialIsRunning = true; - CrestronInvoke.BeginInvoke(o => + Task.Run(() => { foreach (var c in chanNum.ToCharArray()) { @@ -199,6 +268,11 @@ public class DevicePresetsModel : Device OnPresetRecalled(_setTopBox, chanNum); } + /// + /// Dials a preset by its number in the list (starting at 1) using the provided set top box device. If the preset number is out of range, nothing will happen. + /// + /// + /// public void Dial(int presetNum, ISetTopBoxNumericKeypad setTopBox) { if (presetNum <= PresetsList.Count) @@ -207,6 +281,13 @@ public class DevicePresetsModel : Device } } + /// + /// Dials a preset by its channel number using the provided set top box device. If the channel number contains characters that are not 0-9 or '-', those characters will be ignored. + /// If the provided set top box device does not implement the required INumericKeypad interface, dialing will be disabled and a message will be logged. + /// If the model was not initialized with a valid set top box device, dialing will be disabled and a message will be logged. + /// + /// + /// public void Dial(string chanNum, ISetTopBoxNumericKeypad setTopBox) { _dialFunctions = new Dictionary>(10) @@ -243,6 +324,11 @@ public class DevicePresetsModel : Device handler(setTopBox, channel); } + /// + /// Updates the preset at the given index with the provided preset information, then saves the updated presets list to the file. If the index is out of range, nothing will happen. + /// + /// The index of the preset to update, starting at 0 + /// The preset information to update public void UpdatePreset(int index, PresetChannel preset) { if (index >= PresetsList.Count) @@ -257,6 +343,10 @@ public class DevicePresetsModel : Device OnPresetsSaved(); } + /// + /// Updates the entire presets list with the provided list, then saves the updated presets list to the file. If the provided list is null, nothing will happen. + /// + /// public void UpdatePresets(List presets) { PresetsList = presets; @@ -268,10 +358,9 @@ public class DevicePresetsModel : Device private void SavePresets() { - try + lock (_fileOps) { - _fileOps.Enter(); - var pl = new PresetsList {Channels = PresetsList, Name = Name}; + var pl = new PresetsList { Channels = PresetsList, Name = Name }; var json = JsonConvert.SerializeObject(pl, Formatting.Indented); using (var file = File.Open(_filePath, FileMode.Truncate)) @@ -279,11 +368,6 @@ public class DevicePresetsModel : Device file.Write(json, Encoding.UTF8); } } - finally - { - _fileOps.Leave(); - } - } private void OnPresetsSaved() diff --git a/src/PepperDash.Essentials.Core/Queues/GenericQueue.cs b/src/PepperDash.Essentials.Core/Queues/GenericQueue.cs index 8cb26524..09d07a05 100644 --- a/src/PepperDash.Essentials.Core/Queues/GenericQueue.cs +++ b/src/PepperDash.Essentials.Core/Queues/GenericQueue.cs @@ -4,24 +4,35 @@ using System.Threading; using Crestron.SimplSharp; using PepperDash.Core; using Serilog.Events; -using Thread = Crestron.SimplSharpPro.CrestronThread.Thread; namespace PepperDash.Essentials.Core.Queues; + +// TODO: The capacity argument in the constructors should be removed. Now that this class uses System.Threading rather than the Crestron library, there is no longer a thread capacity limit. +// If a capacity limit is needed, it should be implemented by the caller by checking the QueueCount property before enqueuing items and deciding how to handle the situation when the queue is too full (e.g. drop messages, log warnings, etc.) + /// /// Threadsafe processing of queued items with pacing if required /// public class GenericQueue : IQueue { private readonly string _key; + + /// + /// Returns the number of items currently in the queue. This is not threadsafe, so it should only be used for informational purposes and not for processing logic. + /// protected readonly ConcurrentQueue _queue; + + /// + /// The thread that processes the queue items + /// protected readonly Thread _worker; - protected readonly CEvent _waitHandle = new CEvent(); + private readonly object _lock = new(); private bool _delayEnabled; private int _delayTime; - private const Thread.eThreadPriority _defaultPriority = Thread.eThreadPriority.MediumPriority; + private const ThreadPriority _defaultPriority = ThreadPriority.Normal; /// /// If the instance has been disposed. @@ -96,7 +107,7 @@ public class GenericQueue : IQueue /// /// /// - public GenericQueue(string key, int pacing, Thread.eThreadPriority priority) + public GenericQueue(string key, int pacing, ThreadPriority priority) : this(key, priority, 0, pacing) { } @@ -107,7 +118,7 @@ public class GenericQueue : IQueue /// /// /// - public GenericQueue(string key, Thread.eThreadPriority priority, int capacity) + public GenericQueue(string key, ThreadPriority priority, int capacity) : this(key, priority, capacity, 0) { } @@ -119,7 +130,7 @@ public class GenericQueue : IQueue /// /// /// - public GenericQueue(string key, int pacing, Thread.eThreadPriority priority, int capacity) + public GenericQueue(string key, int pacing, ThreadPriority priority, int capacity) : this(key, priority, capacity, pacing) { } @@ -131,21 +142,18 @@ public class GenericQueue : IQueue /// /// /// - protected GenericQueue(string key, Thread.eThreadPriority priority, int capacity, int pacing) + protected GenericQueue(string key, ThreadPriority priority, int capacity, int pacing) { _key = key; - int cap = 25; // sets default - if (capacity > 0) - { - cap = capacity; // overrides default - } _queue = new ConcurrentQueue(); - _worker = new Thread(ProcessQueue, null, Thread.eThreadStartOptions.Running) + _worker = new Thread(ProcessQueue) { Priority = priority, - Name = _key + Name = _key, + IsBackground = true }; + _worker.Start(); SetDelayValues(pacing); } @@ -167,9 +175,8 @@ public class GenericQueue : IQueue /// /// Thread callback /// - /// The action used to process dequeued items /// Null when the thread is exited - private object ProcessQueue(object obj) + private void ProcessQueue() { while (true) { @@ -186,7 +193,7 @@ public class GenericQueue : IQueue if (_delayEnabled) Thread.Sleep(_delayTime); } - catch (ThreadAbortException) + catch (ThreadInterruptedException) { //swallowing this exception, as it should only happen on shut down } @@ -202,12 +209,21 @@ public class GenericQueue : IQueue } } } - else _waitHandle.Wait(); + else + { + lock (_lock) + { + if (_queue.IsEmpty) + Monitor.Wait(_lock); + } + } } - - return null; } + /// + /// Enqueues an item to be processed by the queue thread. If the queue has been disposed, the item will not be enqueued and a message will be logged. + /// + /// public void Enqueue(IQueueMessage item) { if (Disposed) @@ -217,7 +233,8 @@ public class GenericQueue : IQueue } _queue.Enqueue(item); - _waitHandle.Set(); + lock (_lock) + Monitor.Pulse(_lock); } /// @@ -242,18 +259,17 @@ public class GenericQueue : IQueue if (disposing) { - using (_waitHandle) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Disposing..."); - _queue.Enqueue(null); - _waitHandle.Set(); - _worker.Join(); - } + Debug.LogMessage(LogEventLevel.Verbose, this, "Disposing..."); + _queue.Enqueue(null); + lock (_lock) + Monitor.Pulse(_lock); + _worker.Join(); } Disposed = true; } + /// Finalizer in case Dispose is not called. This will clean up the thread, but any items still in the queue will not be processed and could potentially be lost. ~GenericQueue() { Dispose(true); diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs index 9483a6c5..2dd2b0d2 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using Crestron.SimplSharp; +using System.Threading.Tasks; using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; @@ -10,19 +9,37 @@ using Serilog.Events; namespace PepperDash.Essentials.Devices.Common.SoftCodec; +/// +/// Class representing a BlueJeans soft codec running on an in-room PC. +/// public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink { + /// + /// The input port for any video source. + /// public RoutingInputPort AnyVideoIn { get; private set; } + /// + /// The currently active input port, which for this device is always AnyVideoIn + /// This is used by the routing system to determine where to route video sources when this device is a destination + /// public RoutingInputPort CurrentInputPort => AnyVideoIn; #region IRoutingInputs Members + /// + /// Collection of the input ports for this device + /// public RoutingPortCollection InputPorts { get; private set; } #endregion + /// + /// Initializes a new instance of the class. + /// + /// The key for the device. + /// The name of the device. public BlueJeansPc(string key, string name) : base(key, name) { @@ -34,14 +51,25 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink #region IRunRouteAction Members + /// + /// Runs a route action for the specified route key and source list key. Optionally, a callback can be provided to be executed upon successful completion. + /// + /// + /// public void RunRouteAction(string routeKey, string sourceListKey) { RunRouteAction(routeKey, sourceListKey, null); } + /// + /// Runs a route action for the specified route key and source list key. Optionally, a callback can be provided to be executed upon successful completion. + /// + /// + /// + /// public void RunRouteAction(string routeKey, string sourceListKey, Action successCallback) { - CrestronInvoke.BeginInvoke(o => + Task.Run(() => { Debug.LogMessage(LogEventLevel.Debug, this, "Run route action '{0}' on SourceList: {1}", routeKey, sourceListKey); @@ -127,6 +155,7 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink #region IHasCurrentSourceInfoChange Members + /// public string CurrentSourceInfoKey { get; set; } /// @@ -158,6 +187,7 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink } SourceListItem _CurrentSourceInfo; + /// public event SourceInfoChangeHandler CurrentSourceChange; #endregion diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index 65d89d85..e2cc5e67 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -20,6 +20,7 @@ using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; using Serilog.Events; using Feedback = PepperDash.Essentials.Core.Feedback; using System.Text; +using System.Threading.Tasks; namespace PepperDash.Essentials.Devices.Common.VideoCodec; @@ -382,7 +383,7 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu /// protected void SetIsReady() { - CrestronInvoke.BeginInvoke((o) => + Task.Run(() => { try { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs index 071eaf27..b4f28db4 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs @@ -26,8 +26,6 @@ namespace PepperDash.Essentials.AppServer.Messengers /// protected VideoCodecBase Codec { get; private set; } - - /// /// /// @@ -126,7 +124,7 @@ namespace PepperDash.Essentials.AppServer.Messengers //state.CurrentDirectory = PrefixDirectoryFolderItems(directory); state.CurrentDirectory = directory; - CrestronInvoke.BeginInvoke((o) => PostStatusMessage(state)); + Task.Run(() => PostStatusMessage(state)); } } catch (Exception ex) @@ -162,7 +160,6 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Called from base's RegisterWithAppServer method /// - /// protected override void RegisterActions() { try @@ -786,6 +783,10 @@ namespace PepperDash.Essentials.AppServer.Messengers } } + /// + /// Sends the full status of the codec, including active calls, camera status, and directory info if applicable + /// + /// protected virtual void SendFullStatus(string id = null) { if (!Codec.IsReady) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index f0f8caa1..7d90b751 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -103,7 +103,7 @@ namespace PepperDash.Essentials /// public MobileControlWebsocketServer DirectServer => _directServer; - private readonly CCriticalSection _wsCriticalSection = new CCriticalSection(); + private readonly object _wsCriticalSection = new(); /// /// Gets or sets the SystemUrl @@ -206,14 +206,14 @@ namespace PepperDash.Essentials //_receiveQueue = new ReceiveQueue(key, ParseStreamRx); _receiveQueue = new GenericQueue( key + "-rxqueue", - Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, + System.Threading.ThreadPriority.Highest, 25 ); // The queue that will collect the outgoing messages in the order they are received _transmitToServerQueue = new GenericQueue( key + "-txqueue", - Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, + System.Threading.ThreadPriority.Highest, 25 ); @@ -228,7 +228,7 @@ namespace PepperDash.Essentials _transmitToClientsQueue = new GenericQueue( key + "-clienttxqueue", - Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, + System.Threading.ThreadPriority.Highest, 25 ); } @@ -1811,10 +1811,8 @@ namespace PepperDash.Essentials private void ConnectWebsocketClient() { - try + lock (_wsCriticalSection) { - _wsCriticalSection.Enter(); - // set to 99999 to let things work on 4-Series if ( (CrestronEnvironment.ProgramCompatibility & eCrestronSeries.Series4) @@ -1843,10 +1841,6 @@ namespace PepperDash.Essentials TryConnect(); } - finally - { - _wsCriticalSection.Leave(); - } } private void TryConnect() diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index be37ff21..c002e3a9 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -15,6 +15,7 @@ using PepperDash.Essentials.Core.Routing; using System.Threading; using Timeout = Crestron.SimplSharp.Timeout; using Serilog.Events; +using System.Threading.Tasks; namespace PepperDash.Essentials; @@ -108,7 +109,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig if (Debug.DoNotLoadConfigOnNextBoot) { - CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file", + CrestronConsole.AddNewConsoleCommand(s => Task.Run(() => GoWithLoad()), "go", "Loads configuration file", ConsoleAccessLevelEnum.AccessOperator); } From ac393c4885bc4c0e0f0857d65bbb461db1e828ae Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 10 Mar 2026 17:35:23 -0600 Subject: [PATCH 023/161] refactor(force-patch): Remove obsolete GenericHttpClient class in favor of built-in HttpClient --- .../Comm and IR/GenericHttpClient.cs | 179 ------------------ 1 file changed, 179 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs diff --git a/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs b/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs deleted file mode 100644 index d1c3a7c8..00000000 --- a/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Crestron.SimplSharp.Net.Http; -using PepperDash.Core; -using System; - -namespace PepperDash.Essentials.Core; - -[Obsolete("Please use the builtin HttpClient class instead: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines")] - public class GenericHttpClient : Device, IBasicCommunication - { - private readonly HttpClient Client; - - /// - /// Event raised when response is received - /// - public event EventHandler ResponseRecived; - - /// - /// Constructor - /// - /// key of the device - /// name of the device - /// hostname for the HTTP client - public GenericHttpClient(string key, string name, string hostname) - : base(key, name) - { - Client = new HttpClient - { - HostName = hostname - }; - - - } - - /// - /// SendText method - /// - /// the path to send the request to - public void SendText(string path) - { - HttpClientRequest request = new HttpClientRequest(); - string url = string.Format("http://{0}/{1}", Client.HostName, path); - request.Url = new UrlParser(url); - HttpClient.DISPATCHASYNC_ERROR error = Client.DispatchAsyncEx(request, Response, request); - } - - /// - /// SendText method - /// - /// format for the items - /// items to format - public void SendText(string format, params object[] items) - { - HttpClientRequest request = new HttpClientRequest(); - string url = string.Format("http://{0}/{1}", Client.HostName, string.Format(format, items)); - request.Url = new UrlParser(url); - HttpClient.DISPATCHASYNC_ERROR error = Client.DispatchAsyncEx(request, Response, request); - } - - /// - /// SendTextNoResponse method - /// - /// format for the items - /// items to format - public void SendTextNoResponse(string format, params object[] items) - { - HttpClientRequest request = new HttpClientRequest(); - string url = string.Format("http://{0}/{1}", Client.HostName, string.Format(format, items)); - request.Url = new UrlParser(url); - Client.Dispatch(request); - } - - /// - /// Response method - /// - /// response received from the HTTP client - /// error status of the HTTP callback - /// original HTTP client request - private void Response(HttpClientResponse response, HTTP_CALLBACK_ERROR error, object request) - { - if (error == HTTP_CALLBACK_ERROR.COMPLETED) - { - var responseReceived = response; - - if (responseReceived.ContentString.Length > 0) - { - ResponseRecived?.Invoke(this, new GenericHttpClientEventArgs(responseReceived.ContentString, (request as HttpClientRequest).Url.ToString(), error)); - } - } - - } - - - #region IBasicCommunication Members - - /// - /// SendBytes method - /// - /// bytes to send - public void SendBytes(byte[] bytes) - { - throw new NotImplementedException(); - } - - - - #endregion - - #region ICommunicationReceiver Members - - /// - /// BytesReceived event - /// - public event EventHandler BytesReceived; - - /// - /// Connect method - /// - public void Connect() - { - throw new NotImplementedException(); - } - - /// - /// Disconnect method - /// - public void Disconnect() - { - throw new NotImplementedException(); - } - - /// - /// IsConnected property - /// - public bool IsConnected - { - get { return true; } - } - - /// - /// TextReceived event - /// - public event EventHandler TextReceived; - - #endregion - } - - /// - /// Represents a GenericHttpClientEventArgs - /// - public class GenericHttpClientEventArgs : EventArgs - { - /// - /// Gets or sets the ResponseText - /// - public string ResponseText { get; private set; } - - /// - /// Gets or sets the RequestPath - /// - public string RequestPath { get; private set; } - - /// - /// Gets or sets the Error - /// - public HTTP_CALLBACK_ERROR Error { get; set; } - - /// - /// Constructor - /// - /// response text - /// request path - /// error status - public GenericHttpClientEventArgs(string response, string request, HTTP_CALLBACK_ERROR error) - { - ResponseText = response; - RequestPath = request; - Error = error; - } - } \ No newline at end of file From 7137945c943269828d5dbde39b20de651f80e42e Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 12 Mar 2026 13:56:18 -0600 Subject: [PATCH 024/161] refactor: Remove GenericRESTfulClient and GenericRESTfulConstants classes to streamline codebase --- .../GenericRESTfulCommunications/Constants.cs | 38 --- .../GenericRESTfulClient.cs | 255 ------------------ 2 files changed, 293 deletions(-) delete mode 100644 src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs delete mode 100644 src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs diff --git a/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs b/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs deleted file mode 100644 index d8a09e4e..00000000 --- a/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; - -namespace PepperDash.Core.GenericRESTfulCommunications; - - /// - /// Constants - /// -public class GenericRESTfulConstants -{ - /// - /// Generic boolean change - /// - public const ushort BoolValueChange = 1; - /// - /// Generic Ushort change - /// - public const ushort UshrtValueChange = 101; - /// - /// Response Code Ushort change - /// - public const ushort ResponseCodeChange = 102; - /// - /// Generic String chagne - /// - public const ushort StringValueChange = 201; - /// - /// Response string change - /// - public const ushort ResponseStringChange = 202; - /// - /// Error string change - /// - public const ushort ErrorStringChange = 203; -} \ No newline at end of file diff --git a/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs b/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs deleted file mode 100644 index 16a2ee04..00000000 --- a/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharp.Net.Http; -using Crestron.SimplSharp.Net.Https; - -namespace PepperDash.Core.GenericRESTfulCommunications; - - /// - /// Generic RESTful communication class - /// - public class GenericRESTfulClient - { - /// - /// Boolean event handler - /// - public event EventHandler BoolChange; - /// - /// Ushort event handler - /// - public event EventHandler UshrtChange; - /// - /// String event handler - /// - public event EventHandler StringChange; - - /// - /// Constructor - /// - public GenericRESTfulClient() - { - - } - - /// - /// Generic RESTful submit request - /// - /// - /// - /// - /// - /// - /// - public void SubmitRequest(string url, ushort port, ushort requestType, string contentType, string username, string password) - { - if (url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)) - { - SubmitRequestHttps(url, port, requestType, contentType, username, password); - } - else if (url.StartsWith("http:", StringComparison.OrdinalIgnoreCase)) - { - SubmitRequestHttp(url, port, requestType, contentType, username, password); - } - else - { - OnStringChange(string.Format("Invalid URL {0}", url), 0, GenericRESTfulConstants.ErrorStringChange); - } - } - - /// - /// Private HTTP submit request - /// - /// - /// - /// - /// - /// - /// - private void SubmitRequestHttp(string url, ushort port, ushort requestType, string contentType, string username, string password) - { - try - { - HttpClient client = new HttpClient(); - HttpClientRequest request = new HttpClientRequest(); - HttpClientResponse response; - - client.KeepAlive = false; - - if(port >= 1 || port <= 65535) - client.Port = port; - else - client.Port = 80; - - var authorization = ""; - if (!string.IsNullOrEmpty(username)) - authorization = EncodeBase64(username, password); - - if (!string.IsNullOrEmpty(authorization)) - request.Header.SetHeaderValue("Authorization", authorization); - - if (!string.IsNullOrEmpty(contentType)) - request.Header.ContentType = contentType; - - request.Url.Parse(url); - request.RequestType = (Crestron.SimplSharp.Net.Http.RequestType)requestType; - - response = client.Dispatch(request); - - CrestronConsole.PrintLine(string.Format("SubmitRequestHttp Response[{0}]: {1}", response.Code, response.ContentString.ToString())); - - if (!string.IsNullOrEmpty(response.ContentString.ToString())) - OnStringChange(response.ContentString.ToString(), 0, GenericRESTfulConstants.ResponseStringChange); - - if (response.Code > 0) - OnUshrtChange((ushort)response.Code, 0, GenericRESTfulConstants.ResponseCodeChange); - } - catch (Exception e) - { - //var msg = string.Format("SubmitRequestHttp({0}, {1}, {2}) failed:{3}", url, port, requestType, e.Message); - //CrestronConsole.PrintLine(msg); - //ErrorLog.Error(msg); - - CrestronConsole.PrintLine(e.Message); - OnStringChange(e.Message, 0, GenericRESTfulConstants.ErrorStringChange); - } - } - - /// - /// Private HTTPS submit request - /// - /// - /// - /// - /// - /// - /// - private void SubmitRequestHttps(string url, ushort port, ushort requestType, string contentType, string username, string password) - { - try - { - HttpsClient client = new HttpsClient(); - HttpsClientRequest request = new HttpsClientRequest(); - HttpsClientResponse response; - - client.KeepAlive = false; - client.HostVerification = false; - client.PeerVerification = false; - - var authorization = ""; - if (!string.IsNullOrEmpty(username)) - authorization = EncodeBase64(username, password); - - if (!string.IsNullOrEmpty(authorization)) - request.Header.SetHeaderValue("Authorization", authorization); - - if (!string.IsNullOrEmpty(contentType)) - request.Header.ContentType = contentType; - - request.Url.Parse(url); - request.RequestType = (Crestron.SimplSharp.Net.Https.RequestType)requestType; - - response = client.Dispatch(request); - - CrestronConsole.PrintLine(string.Format("SubmitRequestHttp Response[{0}]: {1}", response.Code, response.ContentString.ToString())); - - if(!string.IsNullOrEmpty(response.ContentString.ToString())) - OnStringChange(response.ContentString.ToString(), 0, GenericRESTfulConstants.ResponseStringChange); - - if(response.Code > 0) - OnUshrtChange((ushort)response.Code, 0, GenericRESTfulConstants.ResponseCodeChange); - - } - catch (Exception e) - { - //var msg = string.Format("SubmitRequestHttps({0}, {1}, {2}, {3}, {4}) failed:{5}", url, port, requestType, username, password, e.Message); - //CrestronConsole.PrintLine(msg); - //ErrorLog.Error(msg); - - CrestronConsole.PrintLine(e.Message); - OnStringChange(e.Message, 0, GenericRESTfulConstants.ErrorStringChange); - } - } - - /// - /// Private method to encode username and password to Base64 string - /// - /// - /// - /// authorization - private string EncodeBase64(string username, string password) - { - var authorization = ""; - - try - { - if (!string.IsNullOrEmpty(username)) - { - string base64String = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(string.Format("{0}:{1}", username, password))); - authorization = string.Format("Basic {0}", base64String); - } - } - catch (Exception e) - { - var msg = string.Format("EncodeBase64({0}, {1}) failed:\r{2}", username, password, e); - CrestronConsole.PrintLine(msg); - ErrorLog.Error(msg); - return "" ; - } - - return authorization; - } - - /// - /// Protected method to handle boolean change events - /// - /// - /// - /// - protected void OnBoolChange(bool state, ushort index, ushort type) - { - var handler = BoolChange; - if (handler != null) - { - var args = new BoolChangeEventArgs(state, type); - args.Index = index; - BoolChange(this, args); - } - } - - /// - /// Protected mehtod to handle ushort change events - /// - /// - /// - /// - protected void OnUshrtChange(ushort value, ushort index, ushort type) - { - var handler = UshrtChange; - if (handler != null) - { - var args = new UshrtChangeEventArgs(value, type); - args.Index = index; - UshrtChange(this, args); - } - } - - /// - /// Protected method to handle string change events - /// - /// - /// - /// - protected void OnStringChange(string value, ushort index, ushort type) - { - var handler = StringChange; - if (handler != null) - { - var args = new StringChangeEventArgs(value, type); - args.Index = index; - StringChange(this, args); - } - } - } \ No newline at end of file From 43a9661e08d7fc52d4c4b06e1ec6cb21be580a46 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 23 Mar 2026 11:20:44 -0600 Subject: [PATCH 025/161] Refactor: Removing unused classes that reference Crestron HTTP classes - Removed the HttpLogoServer class and its related functionality from ControlSystem. - Updated ControlSystem to eliminate references to the logo server, including initialization and device checks. - Cleaned up unused variables and methods related to logo server handling. --- src/PepperDash.Core/WebApi/Presets/Preset.cs | 86 -- src/PepperDash.Core/WebApi/Presets/User.cs | 92 -- .../WebApi/Presets/WebApiPasscodeClient.cs | 280 ------ .../Config/Essentials/ConfigUpdater.cs | 259 ----- src/PepperDash.Essentials/ClassDiagram1.cd | 881 ------------------ src/PepperDash.Essentials/ControlSystem.cs | 79 +- src/PepperDash.Essentials/HttpLogoServer.cs | 123 --- 7 files changed, 7 insertions(+), 1793 deletions(-) delete mode 100644 src/PepperDash.Core/WebApi/Presets/Preset.cs delete mode 100644 src/PepperDash.Core/WebApi/Presets/User.cs delete mode 100644 src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs delete mode 100644 src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs delete mode 100644 src/PepperDash.Essentials/ClassDiagram1.cd delete mode 100644 src/PepperDash.Essentials/HttpLogoServer.cs diff --git a/src/PepperDash.Core/WebApi/Presets/Preset.cs b/src/PepperDash.Core/WebApi/Presets/Preset.cs deleted file mode 100644 index 8d174b78..00000000 --- a/src/PepperDash.Core/WebApi/Presets/Preset.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; - -namespace PepperDash.Core.WebApi.Presets; - -/// -/// Represents a preset -/// - public class Preset - { - /// - /// ID of preset - /// - public int Id { get; set; } - - /// - /// User ID - /// - public int UserId { get; set; } - - /// - /// Room Type ID - /// - public int RoomTypeId { get; set; } - - /// - /// Preset Name - /// - public string PresetName { get; set; } - - /// - /// Preset Number - /// - public int PresetNumber { get; set; } - - /// - /// Preset Data - /// - public string Data { get; set; } - - /// - /// Constructor - /// - public Preset() - { - PresetName = ""; - PresetNumber = 1; - Data = "{}"; - } - } - - /// - /// Represents a PresetReceivedEventArgs - /// - public class PresetReceivedEventArgs : EventArgs - { - /// - /// True when the preset is found - /// - public bool LookupSuccess { get; private set; } - - /// - /// S+ helper - /// - public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } } - - /// - /// The preset - /// - public Preset Preset { get; private set; } - - /// - /// For Simpl+ - /// - public PresetReceivedEventArgs() { } - - /// - /// Constructor - /// - /// - /// - public PresetReceivedEventArgs(Preset preset, bool success) - { - LookupSuccess = success; - Preset = preset; - } - } \ No newline at end of file diff --git a/src/PepperDash.Core/WebApi/Presets/User.cs b/src/PepperDash.Core/WebApi/Presets/User.cs deleted file mode 100644 index a20803e4..00000000 --- a/src/PepperDash.Core/WebApi/Presets/User.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; - -namespace PepperDash.Core.WebApi.Presets; - - /// - /// - /// - public class User - { - /// - /// - /// - public int Id { get; set; } - - /// - /// - /// - public string ExternalId { get; set; } - - /// - /// - /// - public string FirstName { get; set; } - - /// - /// - /// - public string LastName { get; set; } - } - - - /// - /// - /// - public class UserReceivedEventArgs : EventArgs - { - /// - /// True when user is found - /// - public bool LookupSuccess { get; private set; } - - /// - /// For stupid S+ - /// - public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } } - - /// - /// - /// - public User User { get; private set; } - - /// - /// For Simpl+ - /// - public UserReceivedEventArgs() { } - - /// - /// Constructor - /// - /// - /// - public UserReceivedEventArgs(User user, bool success) - { - LookupSuccess = success; - User = user; - } - } - - /// - /// Represents a UserAndRoomMessage - /// - public class UserAndRoomMessage - { - /// - /// - /// - public int UserId { get; set; } - - /// - /// - /// - public int RoomTypeId { get; set; } - - /// - /// - /// - public int PresetNumber { get; set; } - } \ No newline at end of file diff --git a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs deleted file mode 100644 index 32e63cda..00000000 --- a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs +++ /dev/null @@ -1,280 +0,0 @@ -extern alias NewtonsoftJson; - -using System; -using Crestron.SimplSharp; // For Basic SIMPL# Classes -using Crestron.SimplSharp.CrestronIO; -using Crestron.SimplSharp.Net.Http; -using Crestron.SimplSharp.Net.Https; -using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; -using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; -using PepperDash.Core.JsonToSimpl; - - -namespace PepperDash.Core.WebApi.Presets; - -/// -/// Passcode client for the WebApi -/// - public class WebApiPasscodeClient : IKeyed - { - /// - /// Notifies when user received - /// - public event EventHandler UserReceived; - - /// - /// Notifies when Preset received - /// - public event EventHandler PresetReceived; - - /// - /// Unique identifier for this instance - /// - public string Key { get; private set; } - - //string JsonMasterKey; - - /// - /// An embedded JsonToSimpl master object. - /// - JsonToSimplGenericMaster J2SMaster; - - string UrlBase; - - string DefaultPresetJsonFilePath; - - User CurrentUser; - - Preset CurrentPreset; - - - /// - /// SIMPL+ can only execute the default constructor. If you have variables that require initialization, please - /// use an Initialize method - /// - public WebApiPasscodeClient() - { - } - - /// - /// Initializes the instance - /// - /// - /// - /// - /// - public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath) - { - Key = key; - //JsonMasterKey = jsonMasterKey; - UrlBase = urlBase; - DefaultPresetJsonFilePath = defaultPresetJsonFilePath; - - J2SMaster = new JsonToSimplGenericMaster(); - J2SMaster.SaveCallback = this.SaveCallback; - J2SMaster.Initialize(jsonMasterKey); - } - - /// - /// Gets the user for a passcode - /// - /// - public void GetUserForPasscode(string passcode) - { - // Bullshit duplicate code here... These two cases should be the same - // except for https/http and the certificate ignores - if (!UrlBase.StartsWith("https")) - return; - var req = new HttpsClientRequest(); - req.Url = new UrlParser(UrlBase + "/api/users/dopin"); - req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post; - req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json")); - req.Header.AddHeader(new HttpsHeader("Accept", "application/json")); - var jo = new JObject(); - jo.Add("pin", passcode); - req.ContentString = jo.ToString(); - - var client = new HttpsClient(); - client.HostVerification = false; - client.PeerVerification = false; - var resp = client.Dispatch(req); - var handler = UserReceived; - if (resp.Code == 200) - { - //CrestronConsole.PrintLine("Received: {0}", resp.ContentString); - var user = JsonConvert.DeserializeObject(resp.ContentString); - CurrentUser = user; - if (handler != null) - UserReceived(this, new UserReceivedEventArgs(user, true)); - } - else - if (handler != null) - UserReceived(this, new UserReceivedEventArgs(null, false)); - } - - /// - /// - /// - /// - /// - /// - /// GetPresetForThisUser method - /// - public void GetPresetForThisUser(int roomTypeId, int presetNumber) - { - if (CurrentUser == null) - { - CrestronConsole.PrintLine("GetPresetForThisUser no user loaded"); - return; - } - - var msg = new UserAndRoomMessage - { - UserId = CurrentUser.Id, - RoomTypeId = roomTypeId, - PresetNumber = presetNumber - }; - - var handler = PresetReceived; - try - { - if (!UrlBase.StartsWith("https")) - return; - var req = new HttpsClientRequest(); - req.Url = new UrlParser(UrlBase + "/api/presets/userandroom"); - req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post; - req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json")); - req.Header.AddHeader(new HttpsHeader("Accept", "application/json")); - req.ContentString = JsonConvert.SerializeObject(msg); - - var client = new HttpsClient(); - client.HostVerification = false; - client.PeerVerification = false; - - // ask for the preset - var resp = client.Dispatch(req); - if (resp.Code == 200) // got it - { - //Debug.Console(1, this, "Received: {0}", resp.ContentString); - var preset = JsonConvert.DeserializeObject(resp.ContentString); - CurrentPreset = preset; - - //if there's no preset data, load the template - if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0) - { - //Debug.Console(1, this, "Loaded preset has no data. Loading default template."); - LoadDefaultPresetData(); - return; - } - - J2SMaster.LoadWithJson(preset.Data); - if (handler != null) - PresetReceived(this, new PresetReceivedEventArgs(preset, true)); - } - else // no existing preset - { - CurrentPreset = new Preset(); - LoadDefaultPresetData(); - if (handler != null) - PresetReceived(this, new PresetReceivedEventArgs(null, false)); - } - } - catch (HttpException e) - { - var resp = e.Response; - Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code); - LoadDefaultPresetData(); - if (handler != null) - PresetReceived(this, new PresetReceivedEventArgs(null, false)); - } - } - - void LoadDefaultPresetData() - { - CurrentPreset = null; - if (!File.Exists(DefaultPresetJsonFilePath)) - { - Debug.Console(0, this, "Cannot load default preset file. Saving will not work"); - return; - } - using (StreamReader sr = new StreamReader(DefaultPresetJsonFilePath)) - { - try - { - var data = sr.ReadToEnd(); - J2SMaster.SetJsonWithoutEvaluating(data); - CurrentPreset = new Preset() { Data = data, UserId = CurrentUser.Id }; - } - catch (Exception e) - { - Debug.Console(0, this, "Error reading default preset JSON: \r{0}", e); - } - } - } - - /// - /// - /// - /// - /// - /// - /// SavePresetForThisUser method - /// - public void SavePresetForThisUser(int roomTypeId, int presetNumber) - { - if (CurrentPreset == null) - LoadDefaultPresetData(); - //return; - - //// A new preset needs to have its numbers set - //if (CurrentPreset.IsNewPreset) - //{ - CurrentPreset.UserId = CurrentUser.Id; - CurrentPreset.RoomTypeId = roomTypeId; - CurrentPreset.PresetNumber = presetNumber; - //} - J2SMaster.Save(); // Will trigger callback when ready - } - - /// - /// After save operation on JSON master happens, send it to server - /// - /// - void SaveCallback(string json) - { - CurrentPreset.Data = json; - - if (!UrlBase.StartsWith("https")) - return; - var req = new HttpsClientRequest(); - req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post; - req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase)); - req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json")); - req.Header.AddHeader(new HttpsHeader("Accept", "application/json")); - req.ContentString = JsonConvert.SerializeObject(CurrentPreset); - - var client = new HttpsClient(); - client.HostVerification = false; - client.PeerVerification = false; - try - { - var resp = client.Dispatch(req); - - // 201=created - // 204=empty content - if (resp.Code == 201) - CrestronConsole.PrintLine("Preset added"); - else if (resp.Code == 204) - CrestronConsole.PrintLine("Preset updated"); - else if (resp.Code == 209) - CrestronConsole.PrintLine("Preset already exists. Cannot save as new."); - else - CrestronConsole.PrintLine("Preset save failed: {0}\r", resp.Code, resp.ContentString); - } - catch (HttpException e) - { - - CrestronConsole.PrintLine("Preset save exception {0}", e.Response.Code); - } - } - } diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs deleted file mode 100644 index a41c43e0..00000000 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigUpdater.cs +++ /dev/null @@ -1,259 +0,0 @@ - - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronIO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Crestron.SimplSharp.Net.Http; -using Crestron.SimplSharpPro.Diagnostics; - -using PepperDash.Core; -using Serilog.Events; - -namespace PepperDash.Essentials.Core.Config; - -public static class ConfigUpdater -{ - public static event EventHandler ConfigStatusChanged; - - public static void GetConfigFromServer(string url) - { - Debug.LogMessage(LogEventLevel.Information, "Attempting to get new config from '{0}'", url); - - // HTTP GET - var req = new HttpClientRequest(); - - try - { - req.RequestType = RequestType.Get; - req.Url.Parse(url); - - new HttpClient().DispatchAsync(req, (r, e) => - { - if (e == HTTP_CALLBACK_ERROR.COMPLETED) - { - if (r.Code == 200) - { - var newConfig = r.ContentString; - - OnStatusUpdate(eUpdateStatus.ConfigFileReceived); - - ArchiveExistingPortalConfigs(); - - CheckForLocalConfigAndDelete(); - - WriteConfigToFile(newConfig); - - RestartProgram(); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Config Update Process Stopped. Failed to get config file from server: {0}", r.Code); - OnStatusUpdate(eUpdateStatus.UpdateFailed); - } - } - else - Debug.LogMessage(LogEventLevel.Information, "Request for config from Server Failed: {0}", e); - }); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Debug, "Error Getting Config from Server: {0}", e); - } - - } - - static void OnStatusUpdate(eUpdateStatus status) - { - var handler = ConfigStatusChanged; - - if(handler != null) - { - handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status)); - } - } - - static void WriteConfigToFile(string configData) - { - var filePath = Global.FilePathPrefix+ "configurationFile-updated.json"; - - try - { - var config = JObject.Parse(configData).ToObject(); - - ConfigWriter.WriteFile(filePath, configData); - - OnStatusUpdate(eUpdateStatus.WritingConfigFile); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Debug, "Error parsing new config: {0}", e); - - OnStatusUpdate(eUpdateStatus.UpdateFailed); - } - } - - /// - /// Checks for any existing portal config files and archives them - /// - static void ArchiveExistingPortalConfigs() - { - var filePath = Global.FilePathPrefix + Global.ConfigFileName; - - var configFiles = ConfigReader.GetConfigFiles(filePath); - - if (configFiles != null) - { - Debug.LogMessage(LogEventLevel.Information, "Existing config files found. Moving to Archive folder."); - - OnStatusUpdate(eUpdateStatus.ArchivingConfigs); - - MoveFilesToArchiveFolder(configFiles); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "No Existing config files found in '{0}'. Nothing to archive", filePath); - } - } - - /// - /// Checks for presence of archive folder and if found deletes contents. - /// Moves any config files to the archive folder and adds a .bak suffix - /// - /// - static void MoveFilesToArchiveFolder(FileInfo[] files) - { - string archiveDirectoryPath = Global.FilePathPrefix + "archive"; - - if (!Directory.Exists(archiveDirectoryPath)) - { - // Directory does not exist, create it - Directory.Create(archiveDirectoryPath); - } - else - { - // Directory exists, first clear any contents - var archivedConfigFiles = ConfigReader.GetConfigFiles(archiveDirectoryPath + Global.DirectorySeparator + Global.ConfigFileName + ".bak"); - - if(archivedConfigFiles != null || archivedConfigFiles.Length > 0) - { - Debug.LogMessage(LogEventLevel.Information, "{0} Existing files found in archive folder. Deleting.", archivedConfigFiles.Length); - - for (int i = 0; i < archivedConfigFiles.Length; i++ ) - { - var file = archivedConfigFiles[i]; - Debug.LogMessage(LogEventLevel.Information, "Deleting archived file: '{0}'", file.FullName); - file.Delete(); - } - } - - } - - // Move any files from the program folder to the archive folder - foreach (var file in files) - { - Debug.LogMessage(LogEventLevel.Information, "Moving config file '{0}' to archive folder", file.FullName); - - // Moves the file and appends the .bak extension - var fileDest = archiveDirectoryPath + "/" + file.Name + ".bak"; - if(!File.Exists(fileDest)) - { - file.MoveTo(fileDest); - } - else - Debug.LogMessage(LogEventLevel.Information, "Cannot move file to archive folder. Existing file already exists with same name: '{0}'", fileDest); - } - } - - /// - /// Checks for LocalConfig folder in file system and deletes if found - /// - static void CheckForLocalConfigAndDelete() - { - var folderPath = Global.FilePathPrefix + ConfigWriter.LocalConfigFolder; - - if (Directory.Exists(folderPath)) - { - OnStatusUpdate(eUpdateStatus.DeletingLocalConfig); - Directory.Delete(folderPath); - Debug.LogMessage(LogEventLevel.Information, "Local Config Found in '{0}'. Deleting.", folderPath); - } - } - - /// - /// Connects to the processor via SSH and restarts the program - /// - static void RestartProgram() - { - Debug.LogMessage(LogEventLevel.Information, "Attempting to Reset Program"); - - OnStatusUpdate(eUpdateStatus.RestartingProgram); - - string response = string.Empty; - - CrestronConsole.SendControlSystemCommand(string.Format("progreset -p:{0}", InitialParametersClass.ApplicationNumber), ref response); - - Debug.LogMessage(LogEventLevel.Debug, "Console Response: {0}", response); - } - -} - - /// - /// Enumeration of eUpdateStatus values - /// - public enum eUpdateStatus - { - /// - /// UpdateStarted status - /// - UpdateStarted, - - /// - /// ConfigFileReceived status - /// - ConfigFileReceived, - - /// - /// ArchivingConfigs status - /// - ArchivingConfigs, - - /// - /// DeletingLocalConfig status - /// - DeletingLocalConfig, - - /// - /// WritingConfigFile status - /// - WritingConfigFile, - - /// - /// RestartingProgram status - /// - RestartingProgram, - - /// - /// UpdateSucceeded status - /// - UpdateSucceeded, - - /// - /// UpdateFailed status - /// - UpdateFailed - } - -public class ConfigStatusEventArgs : EventArgs -{ - public eUpdateStatus UpdateStatus { get; private set; } - - public ConfigStatusEventArgs(eUpdateStatus status) - { - UpdateStatus = status; - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials/ClassDiagram1.cd b/src/PepperDash.Essentials/ClassDiagram1.cd deleted file mode 100644 index aa0f4ed0..00000000 --- a/src/PepperDash.Essentials/ClassDiagram1.cd +++ /dev/null @@ -1,881 +0,0 @@ - - - - - - AAAAAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAIAgA= - Audio\EssentialsVolumeLevelConfig.cs - - - - - - AAAAAAAAEAAAAAAAAAAAAAAAAIAAAAgBAAAAAAAAAAA= - Audio\EssentialsVolumeLevelConfig.cs - - - - - - AAAAAAAIAAAAAAAABAAAACAAIAAAAQAAAAAAAAAAEAA= - Config\ConfigReader.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAA= - Config\DeviceFactory.cs - - - - - - AAAAAAEAAAAAQAIAAAAAAAACAAAAAAAAAAAAAAEAAAA= - Config\EssentialsConfig.cs - - - - - - AAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Config\EssentialsConfig.cs - - - - - - AAAAAAAgEAAAAAAEAAAAAAAAAAAAAAAAAAAAAEAIAIA= - Configuration ORIGINAL\ConfigTieLine.cs - - - - - - AEAAAAAAAAAAgQAAAAAAgAAAAAAAAAAAAAAgEDAADAQ= - Configuration ORIGINAL\Configuration.cs - - - - - - AAAAAAAAAAAAAAQAAAAAAAQAAACAAAAAAAAAAAAAAAA= - Configuration ORIGINAL\Configuration.cs - - - - - - AAAAAAAAAgAAAAAAAEAAAAAAAAAAAAAAAAAgAAAAAAA= - Configuration ORIGINAL\Configuration.cs - - - - - - IQAAAAAEAAAAAAADAAACABQAAAAAAAAAAAAAAAAAAAA= - Configuration ORIGINAL\Configuration.cs - - - - - - AAAAAAAAAAAAAAAAAEAAAAAAAAAAAIAAAAAgAAAIAAA= - Configuration ORIGINAL\ConfigurationHelpers.cs - - - - - - AAAAAAAAAAAQAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAA= - Configuration ORIGINAL\Factories\DmFactory.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAEDAAKAA= - Configuration ORIGINAL\Factories\FactoryHelper.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAABAAAA= - Configuration ORIGINAL\Factories\FactoryHelper.cs - - - - - - AAoAgAAIAEAACAAEAIEIBICAAAAAAABAAAAAAAAAAAA= - ControlSystem.cs - - - - - - AAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAA= - Devices\Amplifier.cs - - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAA= - Room\Cotija\CotijaConfig.cs - - - - - - AAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAA= - Room\Cotija\CotijaConfig.cs - - - - - - CAAABAABAgACCAKGBIAAEyBAFAAACYSAgIAAAAJkAAA= - Room\Cotija\CotijaSystemController.cs - - - - - - AAQIQAAAAAAAAgAAAAAAQAAAAAAAAAAAAAAAABAAAAA= - Room\Cotija\RoomBridges\CotijaBridgeBase.cs - - - - - - AAAIAAAAIABAAkgAABgIAAAAAAAAAEAAAAAAgBAAACA= - Room\Cotija\RoomBridges\CotijaEssentialsHuddleSpaceRoomBridge.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAA= - Room\Cotija\RoomBridges\CotijaEssentialsHuddleSpaceRoomBridge.cs - - - - - - iQFBQAIAAgAAgQAEQACAMABAAABAACAQUAAQAAgCgBA= - Room\Types\EssentialsHuddleSpaceRoom.cs - - - - - - - iQFBQAIAIgQMgQAEQAigMABAAABAADIwUACQAAgCgTE= - Room\Types\EssentialsHuddleVtc1Room.cs - - - - - - - iQAEAAIACkAAAQAOQASgMAAJAABgAAAQQAAQAAgCgBA= - Room\Types\EssentialsPresentationRoom.cs - - - - - - - gQAAEAICECgCAQAEAAAUIwIyAAAAgACiAgAAAQECAgA= - Room\Types\EssentialsRoomBase.cs - - - - - - AICCIAAIGIAAAAAgCAAIAEAAAAAAAAAAAAEAIAAAAAA= - UI\CrestronTouchpanelPropertiesConfig.cs - - - - - - AAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - UI\CrestronTouchpanelPropertiesConfig.cs - - - - - - AAAIAEAAAAAAAAAAAAAAAAEAAAAACKIBAAgACAAAAAA= - UI\EssentialsTouchpanelController.cs - - - - - - AAAAQAAAACAAAAABAAAAAAAAAABAAAAAAAAAAkAAAAA= - UI\HttpLogoServer.cs - - - - - - j+jWCNqEIGzi4UTaTgyn37kpncQJK7L42VMLmMgTE5A= - UI\JoinConstants\UIBoolJoin.cs - - - - - - BAggBIABCQAAGAAQAAAACAACAAAAAAAAAAIAAAAAAAA= - UI\JoinConstants\UISmartObjectJoin.cs - - - - - - BkBgIAgAAggOQAFGAYQIABACgCEBjkSQAUEAASIABCE= - UI\JoinConstants\UIStringlJoin.cs - - - - - - AAQAAAACAAAACAEAAIAAAAAAIABAIAAAABAAAEAAAAA= - UI\JoinConstants\UIUshortJoin.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAA= - UI\SubpageReferenceListActivityItem.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAA= - UI\SubpageReferenceListCallStagingItem.cs - - - - - - AAAABAAAAAAAAAAAAAAAAAAAAgAAAAACAAAAAABgAAA= - UI\SubpageReferenceListSourceItem.cs - - - - - - CAAIkAAAAAAQEAAAEAACAAAAAIAEABAAAgAACAAAAAA= - UIDrivers\enums and base.cs - - - - - - ABAAAAAAAhgAAAAAEAAAAAAAAIAEAAAAAAAAAAAAAAA= - UIDrivers\Essentials\EssentialsPanelMainInterfaceDriver.cs - - - - - - gTEAIIAiCggFNCQ4EA4AWBkAKJCEqAAOS4CKMAQQJQA= - UIDrivers\Essentials\EssentialsPresentationPanelAvFunctionsDriver.cs - - - - - - gRQAIICuAghENHQpEA4IWCkBMJDEsEAEC4CAMARQIBA= - UIDrivers\EssentialsHuddle\EssentialsHuddlePanelAvFunctionsDriver.cs - - - - - - GRwAIYC+oghAeHStEDAIWCdBMADEsBAcDwCAMARYIBg= - UIDrivers\EssentialsHuddleVTC\EssentialsHuddleVtc1PanelAvFunctionsDriver.cs - - - - - - - AAAQgAIAAAAAAAAAEAAAAAQAAAAEAAAAAAAAEAAAgAA= - UIDrivers\JoinedSigInterlock.cs - - - - - - AAAAAAAAAAAAAAAAEAAAQAAABIAEAAAAAAAAAAAAAAA= - UIDrivers\Page Drivers\SingleSubpageModalAndBackDriver.cs - - - - - - AAAAAAAAAAAAAAAAEAAAAAAABAAEAAAAAAAAAAAAAAA= - UIDrivers\Page Drivers\SingleSubpageModalDriver.cs - - - - - - AAAQAAIAAAAAAAAIEAAAAAAAAAAEAAAAAAAAEAAAgAA= - UIDrivers\SigInterlock.cs - - - - - - AAAAAAAACAAACBAAAAAAAAAAAAAAAAAAAAIAAAAAAQA= - UIDrivers\SmartObjectRoomsList.cs - - - - - - AAAAQAAAAAAAAAAAABAAQAAAAAAAAAACAAEAAAAAAAA= - UIDrivers\SmartObjectRoomsList.cs - - - - - - AAAAAAABAAAAAEAAAAAAAAAAAAAAAAAAAQAAAAAAAAA= - UIDrivers\VolumeAndSourceChangeArgs.cs - - - - - - IAGigRBpCgZwAIMSBBIbIgAAImAPtEBiAAgECpJgKQo= - OTHER\Fusion\EssentialsHuddleSpaceFusionSystemControllerBase.cs - - - - - - - AAAAAACgAIQAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA= - OTHER\Fusion\EssentialsHuddleSpaceFusionSystemControllerBase.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAQAAACAAAAAAAAAAAAA= - OTHER\Fusion\EssentialsHuddleSpaceFusionSystemControllerBase.cs - - - - - - AACAAAAAAgAAAIAAAAAAAAAgIBAQAAAAAAAAAAAAAQA= - OTHER\Fusion\EssentialsHuddleVtc1FusionController.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionEventHandlers.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAA= - OTHER\Fusion\FusionEventHandlers.cs - - - - - - AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionProcessorQueries.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAEAAAA= - OTHER\Fusion\FusionProcessorQueries.cs - - - - - - AAICAAAAAAAAAAAAAAAAAACAAAATQAAAAAAAABAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAQAAAAAAQAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAQAAAAAAQAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAACACAAAAQAAAAAAAAAAAAAAAAAAAACAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAACAAAgAAQAAAAAAAAAAAAAAAAAAIAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAgAAQAAAAAAAAAAAAAAAAAAIAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAACAAAAAAQAAAAAAAAAAgAAAAAAAAAAABAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AABCAAAAYEAAEBIBAIAAJAQAQKAYAAAIEAAAEAACCgg= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAAAAAAAEAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAABAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAgAQAAIAAAAAAACAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA= - OTHER\Fusion\FusionRviDataClasses.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAIAgAAQAAAAAAAAEAAAAAAA= - Room\Config\DDVC01RoomPropertiesConfig.cs - - - - - - AAAAAAAAAAAAAAAAAEAAAAQAAAAAAAAAAAAAAAAAAAA= - Room\Config\DDVC01RoomPropertiesConfig.cs - - - - - - AAAAAAAAAAAAgAAAAAAAAAAACAAAAAABAAAAAAAAABA= - Room\Config\EssentialsHuddleRoomPropertiesConfig.cs - - - - - - AAAAAAAAAAAAgAAAAAAAAAAACIAAAAABAAAAAAAAABA= - Room\Config\EssentialsHuddleVtc1PropertiesConfig.cs - - - - - - AAAAAAACAIAAAAAAAAAAABAACAAAAAABAAAAAAAAABA= - Room\Config\EssentialsPresentationPropertiesConfig.cs - - - - - - AAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAEAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AABAEQAAAAEoAAEEAAQAAAACAAAAAAgggAAAAQAAAgA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AAAAAAAAAAEAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAgABAAAAAIAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AAAAAAAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIACQAAAAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AACAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAA= - Room\Config\EssentialsRoomConfig.cs - - - - - - AAAAAAAAAAAAABAACAAAAAAAAAAAAAAAAAAAAAAAAAA= - Room\Config\EssentialsRoomEmergencyConfig.cs - - - - - - AAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAABQAAAAAAAAA= - Room\Config\EssentialsRoomEmergencyConfig.cs - - - - - - AUAAAAgAAAACwAYAAAAAAAEAAAAAAAAAIcAAAEAAAAA= - Room\Cotija\CotijaDdvc01DeviceBridge.cs - - - - - - - AAACAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAA= - Room\Cotija\DeviceTypeInterfaces\IChannelExtensions.cs - - - - - - AAACAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAA= - Room\Cotija\DeviceTypeInterfaces\IColorExtensions.cs - - - - - - AAACAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAA= - Room\Cotija\DeviceTypeInterfaces\IDPadExtensions.cs - - - - - - AAACAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAA= - Room\Cotija\DeviceTypeInterfaces\IDvrExtensions.cs - - - - - - AAACAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAA= - Room\Cotija\DeviceTypeInterfaces\INumericExtensions.cs - - - - - - AAACAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAA= - Room\Cotija\DeviceTypeInterfaces\IPowerExtensions.cs - - - - - - AAACAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAA= - Room\Cotija\DeviceTypeInterfaces\ISetTopBoxControlsExtensions.cs - - - - - - AAACAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAA= - Room\Cotija\DeviceTypeInterfaces\ITransportExtensions.cs - - - - - - gACIAAAAAAICAFAAAACAAAEIIAAAAAQAQAAAABAAAAA= - Room\Cotija\RoomBridges\CotijaDdvc01RoomBridge.cs - - - - - - - AAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Room\Emergency\EsentialsRoomEmergencyContactClosure.cs - - - - - - - AAAAAAAAAAAAAAAASBAAAAAAAAAAAAAABAAAAAAAEAA= - Room\Emergency\EsentialsRoomEmergencyContactClosure.cs - - - - - - AAAQAAAAAiACDAAAGCAACgAIAABECBAgQAAAAQAAAAA= - UIDrivers\EssentialsHuddle\EssentialsHuddleTechPageDriver.cs - - - - - - XAASAoAiAagwAcBAGAUURWQEOHQFAKCmAABCNSSEDPA= - UIDrivers\VC\EssentialsVideoCodecUiDriver.cs - - - - - - AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Room\Types\EssentialsRoomBase.cs - - - - - - AAgAAAAAAAAACBAAACAAAAIBAAAEAAAEAgAAAAAAIAA= - UIDrivers\EssentialsHuddleVTC\EssentialsHuddleVtc1PanelAvFunctionsDriver.cs - - - - - - AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA= - Room\Cotija\Interfaces.cs - - - - - - AAAAAAAAQAAAAAAACAAAAAAAAAAAAAAAAAAAAAEACAA= - Room\Types\EssentialsRoomBase.cs - - - - - - AAAAAAAAACAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAA= - Room\Types\EssentialsRoomBase.cs - - - - - - AAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAIEAAAA= - Room\Types\EssentialsRoomBase.cs - - - - - - AIAAAAAAAAAAAAAAAhBAAAAAAAAAAACYAAAAAAAAAAA= - UIDrivers\enums and base.cs - - - - - - AAAAAAAAAAAAABAAAAAAAAAAAAAAACAAAEAAAAAAAAA= - UIDrivers\enums and base.cs - - - - - - AAAAAAAEAAAAAAAAAIAAAAAAAAQAAAAQAAAAAAAAAAA= - UIDrivers\enums and base.cs - - - - - - AAAAABAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAA= - UIDrivers\enums and base.cs - - - - - - AAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - UIDrivers\enums and base.cs - - - - - - AAAAgCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - UIDrivers\VolumeAndSourceChangeArgs.cs - - - - - - AAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Room\Cotija\RoomBridges\CotijaEssentialsHuddleSpaceRoomBridge.cs - - - - - - AAAAAAAAAAAAACAAACAAAAAAAAAAAAAAAAAAACAAAAA= - UIDrivers\VolumeAndSourceChangeArgs.cs - - - - \ No newline at end of file diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index c002e3a9..fcb48fdd 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -5,7 +5,6 @@ using System.Reflection; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; -using Crestron.SimplSharpPro.CrestronThread; using Crestron.SimplSharpPro.Diagnostics; using PepperDash.Core; using PepperDash.Essentials.Core; @@ -28,10 +27,8 @@ namespace PepperDash.Essentials; /// provides methods for platform determination, configuration loading, and system teardown. public class ControlSystem : CrestronControlSystem, ILoadConfig { - private HttpLogoServer LogoServer; - - private Timer _startTimer; - private ManualResetEventSlim _initializeEvent; + private Timer startTimer; + private ManualResetEventSlim initializeEvent; private const long StartupTime = 500; /// @@ -79,16 +76,16 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig { Debug.LogMessage(LogEventLevel.Debug, "******************* Initializing System **********************"); - _startTimer = new Timer(StartSystem, preventInitializationComplete, StartupTime, Timeout.Infinite); + startTimer = new Timer(StartSystem, preventInitializationComplete, StartupTime, Timeout.Infinite); - _initializeEvent = new ManualResetEventSlim(false); + initializeEvent = new ManualResetEventSlim(false); DeviceManager.AllDevicesRegistered += (o, a) => { - _initializeEvent.Set(); + initializeEvent.Set(); }; - _initializeEvent.Wait(30000); + initializeEvent.Wait(30000); Debug.LogMessage(LogEventLevel.Debug, "******************* System Initialization Complete **********************"); @@ -96,7 +93,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig } else { - _startTimer = new Timer(StartSystem, preventInitializationComplete, StartupTime, Timeout.Infinite); + startTimer = new Timer(StartSystem, preventInitializationComplete, StartupTime, Timeout.Infinite); } } @@ -357,7 +354,6 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig { LoadDevices(); LoadRooms(); - LoadLogoServer(); DeviceManager.ActivateAll(); @@ -495,67 +491,6 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig } - /// - /// Fires up a logo server if not already running - /// - void LoadLogoServer() - { - if (ConfigReader.ConfigObject.Rooms == null) - { - Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup."); - return; - } - - if ( - !ConfigReader.ConfigObject.Rooms.Any( - CheckRoomConfig)) - { - Debug.LogMessage(LogEventLevel.Information, "No rooms configured to use system Logo server. Bypassing Logo server startup"); - return; - } - - try - { - LogoServer = new HttpLogoServer(8080, Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo"); - } - catch (Exception) - { - Debug.LogMessage(LogEventLevel.Information, "NOTICE: Logo server cannot be started. Likely already running in another program"); - } - } - - private bool CheckRoomConfig(DeviceConfig c) - { - string logoDark = null; - string logoLight = null; - string logo = null; - - try - { - if (c.Properties["logoDark"] != null) - { - logoDark = c.Properties["logoDark"].Value("type"); - } - - if (c.Properties["logoLight"] != null) - { - logoLight = c.Properties["logoLight"].Value("type"); - } - - if (c.Properties["logo"] != null) - { - logo = c.Properties["logo"].Value("type"); - } - - return ((logoDark != null && logoDark == "system") || - (logoLight != null && logoLight == "system") || (logo != null && logo == "system")); - } - catch - { - Debug.LogMessage(LogEventLevel.Information, "Unable to find logo information in any room config"); - return false; - } - } private static void LoadAssets() { diff --git a/src/PepperDash.Essentials/HttpLogoServer.cs b/src/PepperDash.Essentials/HttpLogoServer.cs deleted file mode 100644 index 174ebd7e..00000000 --- a/src/PepperDash.Essentials/HttpLogoServer.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronIO; -using Crestron.SimplSharp.Net.Http; - -using PepperDash.Core; -using Serilog.Events; - -namespace PepperDash.Essentials; - -public class HttpLogoServer -{ - /// - /// - /// - readonly HttpServer _server; - - /// - /// - /// - readonly string _fileDirectory; - - /// - /// - /// - public static Dictionary ExtensionContentTypes; - - /// - /// - /// - /// - /// - public HttpLogoServer(int port, string directory) - { - ExtensionContentTypes = new Dictionary - { - //{ ".css", "text/css" }, - //{ ".htm", "text/html" }, - //{ ".html", "text/html" }, - { ".jpg", "image/jpeg" }, - { ".jpeg", "image/jpeg" }, - //{ ".js", "application/javascript" }, - //{ ".json", "application/json" }, - //{ ".map", "application/x-navimap" }, - { ".pdf", "application/pdf" }, - { ".png", "image/png" }, - //{ ".txt", "text/plain" }, - }; - - _server = new HttpServer {Port = port}; - _fileDirectory = directory; - _server.OnHttpRequest += Server_OnHttpRequest; - _server.Open(); - - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - } - - /// - /// - /// - void Server_OnHttpRequest(object sender, OnHttpRequestArgs args) - { - var path = args.Request.Path; - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "HTTP Request with path: '{requestPath:l}'", args.Request.Path); - - try - { - if (File.Exists(_fileDirectory + path)) - { - var filePath = path.Replace('/', '\\'); - var localPath = string.Format(@"{0}{1}", _fileDirectory, filePath); - - Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server attempting to find file: '{localPath:l}'", localPath); - if (File.Exists(localPath)) - { - args.Response.Header.ContentType = GetContentType(new FileInfo(localPath).Extension); - args.Response.ContentStream = new FileStream(localPath, FileMode.Open, FileAccess.Read); - } - else - { - Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server Cannot find file '{localPath:l}'", localPath); - args.Response.ContentString = string.Format("Not found: '{0}'", filePath); - args.Response.Code = 404; - } - } - else - { - Debug.LogMessage(LogEventLevel.Verbose, "HTTP Logo Server: '{file:l}' does not exist", _fileDirectory + path); - args.Response.ContentString = string.Format("Not found: '{0}'", _fileDirectory + path); - args.Response.Code = 404; - } - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Error, "Exception getting file: {exception}", ex.Message, ex.StackTrace); - Debug.LogMessage(LogEventLevel.Verbose, "Stack Trace: {stackTrace}", ex.StackTrace); - - args.Response.Code = 400; - args.Response.ContentString = string.Format("invalid request"); - } - } - - /// - /// - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping) - _server.Close(); - } - - /// - /// - /// - /// - /// - public static string GetContentType(string extension) - { - var type = ExtensionContentTypes.ContainsKey(extension) ? ExtensionContentTypes[extension] : "text/plain"; - return type; - } -} \ No newline at end of file From b4d53dbe0e602c791fe5790e5b11596a4e0851c4 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 26 Mar 2026 13:49:23 -0600 Subject: [PATCH 026/161] refactor: routing interfaces and implementations to support current sources - Updated ICurrentSources interface to use IRoutingSource instead of SourceListItem. - Introduced CurrentSourcesChangedEventArgs for detailed event notifications. - Modified IRoutingOutputs and IRoutingSource interfaces to include JSON properties for serialization. - Enhanced IRoutingSink and related interfaces to implement ICurrentSources for better source management. - Refactored RoutingFeedbackManager to utilize new current source handling. - Updated GenericAudioOut, GenericSink, and BlueJeansPc classes to implement new current source logic. - Adjusted MobileControl messengers to accommodate changes in current source handling. - Removed deprecated destination handling in MobileControlEssentialsRoomBridge. --- .../Comm/GenericTcpIpClient.cs | 6 - .../Devices/DestinationListItem.cs | 8 +- .../Devices/SourceListItem.cs | 6 +- .../Fusion/IEssentialsRoomFusionController.cs | 57 +++++--- .../Room/Interfaces.cs | 12 -- .../Routing/ICurrentSources.cs | 48 +++++- .../Routing/IRoutingOutputs.cs | 4 +- .../Routing/IRoutingSink.cs | 9 +- .../Routing/IRoutingSinkWithFeedback.cs | 5 +- .../Routing/IRoutingSource.cs | 6 +- .../Routing/RoutingFeedbackManager.cs | 137 +++++------------- .../Audio/GenericAudioOut.cs | 98 ++++++++++--- .../Displays/DisplayBase.cs | 55 ++----- .../Generic/GenericSink.cs | 87 ++++++----- .../SoftCodec/BlueJeansPc.cs | 82 ++++++++++- .../SoftCodec/GenericSoftCodec.cs | 76 +++++++++- .../Messengers/CurrentSourcesMessenger.cs | 3 +- .../Messengers/IRunRouteActionMessenger.cs | 59 +------- .../MobileControlEssentialsRoomBridge.cs | 31 ---- 19 files changed, 439 insertions(+), 350 deletions(-) diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs index 52670899..54298cc3 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs @@ -131,12 +131,6 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut /// public string ClientStatusText { get { return ClientStatus.ToString(); } } - /// - /// Ushort representation of client status - /// - [Obsolete] - public ushort UClientStatus { get { return (ushort)ClientStatus; } } - /// /// Connection failure reason /// diff --git a/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs b/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs index 03f7af0e..3e61bd80 100644 --- a/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; namespace PepperDash.Essentials.Core; @@ -11,23 +12,22 @@ namespace PepperDash.Essentials.Core; /// public class DestinationListItem { - /// /// Gets or sets the key identifier for the sink device that this destination represents. /// [JsonProperty("sinkKey")] public string SinkKey { get; set; } - private EssentialsDevice _sinkDevice; + private IRoutingSink _sinkDevice; /// /// Gets the actual device instance for this destination. /// Lazily loads the device from the DeviceManager using the SinkKey. /// [JsonIgnore] - public EssentialsDevice SinkDevice + public IRoutingSink SinkDevice { - get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as EssentialsDevice); } + get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as IRoutingSink); } } /// diff --git a/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs b/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs index 7ffd711d..97ff9efe 100644 --- a/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs @@ -43,17 +43,17 @@ public class SourceListItem /// Returns the source Device for this, if it exists in DeviceManager /// [JsonIgnore] - public Device SourceDevice + public IRoutingSource SourceDevice { get { if (_SourceDevice == null) - _SourceDevice = DeviceManager.GetDeviceForKey(SourceKey) as Device; + _SourceDevice = DeviceManager.GetDeviceForKey(SourceKey); return _SourceDevice; } } - private Device _SourceDevice; + private IRoutingSource _SourceDevice; /// /// Gets either the source's Name or this AlternateName property, if diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index 564fa395..0efdf5e1 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -1236,11 +1236,14 @@ namespace PepperDash.Essentials.Core.Fusion uint i = 0; foreach (var kvp in setTopBoxes) { - 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 + if (kvp.Value.SourceDevice is Device device) { - break; + TryAddRouteActionSigs(JoinMap.Display1SetTopBoxSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1SetTopBoxSourceStart.JoinNumber + i, kvp.Key, device); + i++; + if (i > JoinMap.Display1SetTopBoxSourceStart.JoinSpan) // We only have five spots + { + break; + } } } @@ -1248,11 +1251,14 @@ namespace PepperDash.Essentials.Core.Fusion i = 0; foreach (var kvp in discPlayers) { - 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 + if (kvp.Value.SourceDevice is Device device) { - break; + TryAddRouteActionSigs(JoinMap.Display1DiscPlayerSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1DiscPlayerSourceStart.JoinNumber + i, kvp.Key, device); + i++; + if (i > JoinMap.Display1DiscPlayerSourceStart.JoinSpan) // We only have five spots + { + break; + } } } @@ -1260,11 +1266,14 @@ namespace PepperDash.Essentials.Core.Fusion i = 0; foreach (var kvp in laptops) { - 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??? + if (kvp.Value.SourceDevice is Device device) { - break; + TryAddRouteActionSigs(JoinMap.Display1LaptopSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1LaptopSourceStart.JoinNumber + i, kvp.Key, device); + i++; + if (i > JoinMap.Display1LaptopSourceStart.JoinSpan) // We only have ten spots??? + { + break; + } } } @@ -1745,22 +1754,24 @@ namespace PepperDash.Essentials.Core.Fusion return; } - var dev = info.SourceDevice; - if (type == ChangeType.WillChange) + if (info.SourceDevice is Device dev) { - if (_sourceToFeedbackSigs.ContainsKey(dev)) + if (type == ChangeType.WillChange) { - _sourceToFeedbackSigs[dev].BoolValue = false; + if (_sourceToFeedbackSigs.ContainsKey(dev)) + { + _sourceToFeedbackSigs[dev].BoolValue = false; + } } - } - else - { - if (_sourceToFeedbackSigs.ContainsKey(dev)) + else { - _sourceToFeedbackSigs[dev].BoolValue = true; + if (_sourceToFeedbackSigs.ContainsKey(dev)) + { + _sourceToFeedbackSigs[dev].BoolValue = true; + } + //var name = (room == null ? "" : room.Name); + CurrentRoomSourceNameSig.InputSig.StringValue = dev.Name; } - //var name = (room == null ? "" : room.Name); - CurrentRoomSourceNameSig.InputSig.StringValue = info.SourceDevice.Name; } } diff --git a/src/PepperDash.Essentials.Core/Room/Interfaces.cs b/src/PepperDash.Essentials.Core/Room/Interfaces.cs index c0041a9a..fd55755a 100644 --- a/src/PepperDash.Essentials.Core/Room/Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Room/Interfaces.cs @@ -28,18 +28,6 @@ public interface IHasDefaultDisplay IRoutingSink DefaultDisplay { get; } } -/// -/// For rooms with multiple displays -/// -[Obsolete("This interface is being deprecated in favor of using destination lists for routing to multiple displays.")] -public interface IHasMultipleDisplays -{ - /// - /// A dictionary of displays in the room with the key being the type of display (presentation, calling, etc.) and the value being the display device - /// - Dictionary Displays { get; } -} - /// /// For rooms with routing /// diff --git a/src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs b/src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs index a8940d08..eb575815 100644 --- a/src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs +++ b/src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs @@ -6,6 +6,7 @@ namespace PepperDash.Essentials.Core.Routing /// /// The current sources for the room, keyed by eRoutingSignalType. /// This allows for multiple sources to be tracked, such as audio and video. + /// Intended to be implemented on a DestinationListItem to provide access to the current sources for the room in its context. /// /// /// This interface is used to provide access to the current sources in a room, @@ -17,7 +18,7 @@ namespace PepperDash.Essentials.Core.Routing /// Gets the current sources for the room, keyed by eRoutingSignalType. /// This dictionary contains the current source for each signal type, such as audio, video, and control signals. /// - Dictionary CurrentSources { get; } + Dictionary CurrentSources { get; } /// /// Gets the current source keys for the room, keyed by eRoutingSignalType. @@ -28,16 +29,51 @@ namespace PepperDash.Essentials.Core.Routing /// /// Event raised when the current sources change. /// - event EventHandler CurrentSourcesChanged; + event EventHandler CurrentSourcesChanged; /// /// Sets the current source for a specific signal type. /// This method updates the current source for the specified signal type and notifies any subscribers of the change. /// /// The signal type to update. - /// The key for the source list. - /// The source list item to set as the current source. - void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem); + /// The source device to set as the current source. + void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice); + } -} + + + /// + /// Event arguments for the CurrentSourcesChanged event, providing details about the signal type and the previous and new sources. + /// + public class CurrentSourcesChangedEventArgs : EventArgs + { + /// + /// Gets the signal type for which the current source has changed. + /// + public eRoutingSignalType SignalType { get; } + + /// + /// Gets the previous source for the signal type before the change occurred. + /// + public IRoutingSource PreviousSource { get; } + + /// + /// Gets the new source for the signal type after the change occurred. + /// + public IRoutingSource NewSource { get; } + + /// + /// Initializes a new instance of the CurrentSourcesChangedEventArgs class with the specified signal type, previous source, and new source. + /// + /// The signal type for which the current source has changed. + /// The previous source for the signal type before the change occurred. + /// The new source for the signal type after the change occurred. + public CurrentSourcesChangedEventArgs(eRoutingSignalType signalType, IRoutingSource previousSource, IRoutingSource newSource) + { + SignalType = signalType; + PreviousSource = previousSource; + NewSource = newSource; + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingOutputs.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingOutputs.cs index da2f6362..702a2367 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingOutputs.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingOutputs.cs @@ -1,4 +1,5 @@ -using PepperDash.Core; +using Newtonsoft.Json; +using PepperDash.Core; namespace PepperDash.Essentials.Core; @@ -11,6 +12,7 @@ public interface IRoutingOutputs : IKeyed /// /// Collection of Output Ports /// + [JsonProperty("outputPorts")] RoutingPortCollection OutputPorts { get; } } diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs index b8af02d9..9426474a 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs @@ -1,3 +1,4 @@ +using PepperDash.Core; using PepperDash.Essentials.Core.Routing; namespace PepperDash.Essentials.Core; @@ -5,7 +6,7 @@ namespace PepperDash.Essentials.Core; /// /// Defines the contract for IRoutingSink /// -public interface IRoutingSink : IRoutingInputs, IHasCurrentSourceInfoChange +public interface IRoutingSink : IRoutingInputs, IKeyName, ICurrentSources { } @@ -20,10 +21,4 @@ public interface IRoutingSinkWithInputPort : IRoutingSink RoutingInputPort CurrentInputPort { get; } } -/// -/// Interface for routing sinks that have access to the current source information. -/// -public interface IRoutingSinkWithCurrentSources : IRoutingSink, ICurrentSources -{ -} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs index 63f51953..d4f9c484 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs @@ -5,8 +5,9 @@ /// /// Defines the contract for IRoutingSinkWithFeedback /// -public interface IRoutingSinkWithFeedback : IRoutingSinkWithSwitching +public interface IRoutingSinkWithFeedback : IRoutingSink { - } + + diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSource.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSource.cs index 5eaaabf8..880d947f 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSource.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSource.cs @@ -1,8 +1,10 @@ -namespace PepperDash.Essentials.Core; +using PepperDash.Core; + +namespace PepperDash.Essentials.Core; /// /// Marker interface to identify a device that acts as the origin of a signal path (). /// -public interface IRoutingSource : IRoutingOutputs +public interface IRoutingSource : IRoutingOutputs, IKeyName { } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 152b245a..69f41d64 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -9,15 +9,15 @@ namespace PepperDash.Essentials.Core.Routing; /// Manages routing feedback by subscribing to route changes on midpoint and sink devices, /// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. /// -public class RoutingFeedbackManager: EssentialsDevice +public class RoutingFeedbackManager : EssentialsDevice { /// /// Initializes a new instance of the class. /// /// The unique key for this manager device. /// The name of this manager device. - public RoutingFeedbackManager(string key, string name): base(key, name) - { + public RoutingFeedbackManager(string key, string name) : base(key, name) + { AddPreActivationAction(SubscribeForMidpointFeedback); AddPreActivationAction(SubscribeForSinkFeedback); } @@ -41,12 +41,12 @@ public class RoutingFeedbackManager: EssentialsDevice /// private void SubscribeForSinkFeedback() { - var sinkDevices = DeviceManager.AllDevices.OfType(); + var sinkDevices = DeviceManager.AllDevices.OfType(); - foreach (var device in sinkDevices) - { - device.InputChanged += HandleSinkUpdate; - } + foreach (var device in sinkDevices) + { + device.InputChanged += HandleSinkUpdate; + } } /// @@ -59,7 +59,7 @@ public class RoutingFeedbackManager: EssentialsDevice { try { - var devices = DeviceManager.AllDevices.OfType(); + var devices = DeviceManager.AllDevices.OfType(); foreach (var device in devices) { @@ -96,13 +96,13 @@ public class RoutingFeedbackManager: EssentialsDevice /// /// The destination sink device to update. /// The currently selected input port on the destination device. - private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort) - { + private void UpdateDestination(IRoutingSink destination, RoutingInputPort inputPort) + { // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key); - if(inputPort == null) + if (inputPort == null) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Destination {destination} has not reported an input port yet", this,destination.Key); + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Destination {destination} has not reported an input port yet", this, destination.Key); return; } @@ -116,19 +116,10 @@ public class RoutingFeedbackManager: EssentialsDevice if (firstTieLine == null) { Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No tieline found for inputPort {inputPort}. Clearing current source", this, inputPort); - - var tempSourceListItem = new SourceListItem - { - SourceKey = "$transient", - Name = inputPort.Key, - }; - - - destination.CurrentSourceInfo = tempSourceListItem; ; - destination.CurrentSourceInfoKey = "$transient"; return; } - } catch (Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); return; @@ -136,6 +127,7 @@ public class RoutingFeedbackManager: EssentialsDevice // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Getting source for first TieLine {tieLine}", this, firstTieLine); + TieLine sourceTieLine; try { @@ -145,91 +137,34 @@ public class RoutingFeedbackManager: EssentialsDevice { Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route found to source for inputPort {inputPort}. Clearing current source", this, inputPort); - var tempSourceListItem = new SourceListItem - { - SourceKey = "$transient", - Name = "None", - }; - destination.CurrentSourceInfo = tempSourceListItem; - destination.CurrentSourceInfoKey = string.Empty; + destination.SetCurrentSource(sourceTieLine.Type, null); + + return; } - } catch(Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); return; } - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine); - - // Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet. - var room = DeviceManager.AllDevices.OfType().FirstOrDefault((r) => { - if(r is IHasMultipleDisplays roomMultipleDisplays) - { - return roomMultipleDisplays.Displays.Any(d => d.Value.Key == destination.Key); - } - - if(r is IHasDefaultDisplay roomDefaultDisplay) - { - return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; - } - - return false; - }); - - if(room == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No room found for display {destination}", this, destination.Key); - return; - } - - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found room {room} for destination {destination}", this, room.Key, destination.Key); - - var sourceList = ConfigReader.ConfigObject.GetSourceListForKey(room.SourceListKey); - - if (sourceList == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", this, room.SourceListKey, sourceTieLine); - return; - } - - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key); - - var sourceListItem = sourceList.FirstOrDefault(sli => { - //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, - // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", - // this, - // sli.Key, - // sli.Value.SourceKey, - // sourceTieLine.SourcePort.ParentDevice.Key); - - return sli.Value.SourceKey.Equals(sourceTieLine.SourcePort.ParentDevice.Key,StringComparison.InvariantCultureIgnoreCase); - }); - - var source = sourceListItem.Value; - var sourceKey = sourceListItem.Key; - - if (source == null) + if (sourceTieLine.SourcePort.ParentDevice == null) { Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No source found for device {key}. Creating transient source for {destination}", this, sourceTieLine.SourcePort.ParentDevice.Key, destination); - var tempSourceListItem = new SourceListItem - { - SourceKey = "$transient", - Name = sourceTieLine.SourcePort.Key, - }; + destination.SetCurrentSource(sourceTieLine.Type, null); - destination.CurrentSourceInfoKey = "$transient"; - destination.CurrentSourceInfo = tempSourceListItem; return; } //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Got Source {@source} with key {sourceKey}", this, source, sourceKey); - destination.CurrentSourceInfoKey = sourceKey; - destination.CurrentSourceInfo = source; - + if (sourceTieLine.SourcePort.ParentDevice is IRoutingSource sourceDevice) + { + destination.SetCurrentSource(sourceTieLine.Type, sourceDevice); + } } /// @@ -249,13 +184,14 @@ public class RoutingFeedbackManager: EssentialsDevice { // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint); - if(midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0) + if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Midpoint {midpointKey} has no routes",this, midpoint.Key); + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Midpoint {midpointKey} has no routes", this, midpoint.Key); return null; } - var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route => { + var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route => + { //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine); return route.OutputPort != null && route.InputPort != null && route.OutputPort?.Key == tieLine.SourcePort.Key && route.OutputPort?.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key; @@ -269,9 +205,11 @@ public class RoutingFeedbackManager: EssentialsDevice //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint); - nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => { + nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => + { //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key); - return tl.DestinationPort.Key == currentRoute.InputPort.Key && tl.DestinationPort.ParentDevice.Key == currentRoute.InputPort.ParentDevice.Key; }); + return tl.DestinationPort.Key == currentRoute.InputPort.Key && tl.DestinationPort.ParentDevice.Key == currentRoute.InputPort.ParentDevice.Key; + }); if (nextTieLine != null) { @@ -292,13 +230,14 @@ public class RoutingFeedbackManager: EssentialsDevice return tieLine; } - nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => tl.DestinationPort.Key == tieLine.SourcePort.Key && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key ); + nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => tl.DestinationPort.Key == tieLine.SourcePort.Key && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key); if (nextTieLine != null) { return GetRootTieLine(nextTieLine); } - } catch (Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex); return null; diff --git a/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs b/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs index 9b8dd2e5..252e4dc4 100644 --- a/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs +++ b/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; - +using PepperDash.Essentials.Core.Routing; using Serilog.Events; namespace PepperDash.Essentials.Devices.Common; @@ -13,33 +14,19 @@ namespace PepperDash.Essentials.Devices.Common; /// public class GenericAudioOut : EssentialsDevice, IRoutingSink { + /// public RoutingInputPort CurrentInputPort => AnyAudioIn; - public event SourceInfoChangeHandler CurrentSourceChange; - public string CurrentSourceInfoKey { get; set; } - public SourceListItem CurrentSourceInfo - { - get - { - return _CurrentSourceInfo; - } - set - { - if (value == _CurrentSourceInfo) return; + /// + public Dictionary CurrentSources { get; private set; } - var handler = CurrentSourceChange; + /// + public Dictionary CurrentSourceKeys { get; private set; } - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.WillChange); + /// + public event System.EventHandler CurrentSourcesChanged; - _CurrentSourceInfo = value; - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.DidChange); - } - } - SourceListItem _CurrentSourceInfo; /// /// Gets or sets the AnyAudioIn @@ -56,6 +43,66 @@ public class GenericAudioOut : EssentialsDevice, IRoutingSink { AnyAudioIn = new RoutingInputPort(RoutingPortNames.AnyAudioIn, eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio, null, this); + + CurrentSources = new Dictionary + { + { eRoutingSignalType.Audio, null }, + }; + + CurrentSourceKeys = new Dictionary + { + { eRoutingSignalType.Audio, string.Empty }, + }; + } + + /// + public virtual void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice) + { + foreach (eRoutingSignalType type in System.Enum.GetValues(typeof(eRoutingSignalType))) + { + var flagValue = System.Convert.ToInt32(type); + // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). + // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. + if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) + { + this.LogDebug("Skipping {type}", type); + continue; + } + + this.LogDebug("setting {type}", type); + + var previousSource = CurrentSources[type]; + + if (signalType.HasFlag(type)) + { + UpdateCurrentSources(type, previousSource, sourceDevice); + } + } + } + + private void UpdateCurrentSources(eRoutingSignalType signalType, IRoutingSource previousSource, IRoutingSource sourceDevice) + { + if (CurrentSources.ContainsKey(signalType)) + { + CurrentSources[signalType] = sourceDevice; + } + else + { + CurrentSources.Add(signalType, sourceDevice); + } + + // Update the current source key for the specified signal type + if (CurrentSourceKeys.ContainsKey(signalType)) + { + CurrentSourceKeys[signalType] = sourceDevice.Key; + } + else + { + CurrentSourceKeys.Add(signalType, sourceDevice.Key); + } + + // Raise the CurrentSourcesChanged event + CurrentSourcesChanged?.Invoke(this, new CurrentSourcesChangedEventArgs(signalType, previousSource, sourceDevice)); } #region IRoutingInputs Members @@ -116,13 +163,20 @@ public class GenericAudioOutWithVolume : GenericAudioOut, IHasVolumeDevice } +/// +/// Factory for creating GenericAudioOutWithVolume devices +/// public class GenericAudioOutWithVolumeFactory : EssentialsDeviceFactory { + /// + /// Constructor for GenericAudioOutWithVolumeFactory + /// public GenericAudioOutWithVolumeFactory() { TypeNames = new List() { "genericaudiooutwithvolume" }; } + /// public override EssentialsDevice BuildDevice(DeviceConfig dc) { Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new GenericAudioOutWithVolumeFactory Device"); diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs index fba36564..5f35e90a 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs @@ -48,48 +48,20 @@ public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, /// public event InputChangedEventHandler InputChanged; - /// - /// Event that is raised when the current source information changes. - /// - public event SourceInfoChangeHandler CurrentSourceChange; - /// /// Gets or sets the CurrentSourceInfoKey /// public string CurrentSourceInfoKey { get; set; } - /// - /// Gets or sets the current source information for the display. - /// - public SourceListItem CurrentSourceInfo - { - get - { - return _CurrentSourceInfo; - } - set - { - if (value == _CurrentSourceInfo) return; - - var handler = CurrentSourceChange; - - handler?.Invoke(_CurrentSourceInfo, ChangeType.WillChange); - - _CurrentSourceInfo = value; - - handler?.Invoke(_CurrentSourceInfo, ChangeType.DidChange); - } - } - SourceListItem _CurrentSourceInfo; /// - public Dictionary CurrentSources { get; private set; } + public Dictionary CurrentSources { get; private set; } /// public Dictionary CurrentSourceKeys { get; private set; } /// - public event EventHandler CurrentSourcesChanged; + public event EventHandler CurrentSourcesChanged; /// /// Gets feedback indicating whether the display is currently cooling down after being powered off. @@ -163,7 +135,7 @@ public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, InputPorts = new RoutingPortCollection(); - CurrentSources = new Dictionary + CurrentSources = new Dictionary { { eRoutingSignalType.Audio, null }, { eRoutingSignalType.Video, null }, @@ -388,7 +360,7 @@ public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, } /// - public virtual void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + public virtual void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice) { foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) { @@ -403,35 +375,38 @@ public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, this.LogDebug("setting {type}", type); + var previousSource = CurrentSources[type]; + if (signalType.HasFlag(type)) { - UpdateCurrentSources(type, sourceListKey, sourceListItem); + UpdateCurrentSources(type, previousSource, sourceDevice); } } - // Raise the CurrentSourcesChanged event - CurrentSourcesChanged?.Invoke(this, EventArgs.Empty); } - private void UpdateCurrentSources(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) + private void UpdateCurrentSources(eRoutingSignalType signalType, IRoutingSource previousSource, IRoutingSource sourceDevice) { if (CurrentSources.ContainsKey(signalType)) { - CurrentSources[signalType] = sourceListItem; + CurrentSources[signalType] = sourceDevice; } else { - CurrentSources.Add(signalType, sourceListItem); + CurrentSources.Add(signalType, sourceDevice); } // Update the current source key for the specified signal type if (CurrentSourceKeys.ContainsKey(signalType)) { - CurrentSourceKeys[signalType] = sourceListKey; + CurrentSourceKeys[signalType] = sourceDevice.Key; } else { - CurrentSourceKeys.Add(signalType, sourceListKey); + CurrentSourceKeys.Add(signalType, sourceDevice.Key); } + + // Raise the CurrentSourcesChanged event + CurrentSourcesChanged?.Invoke(this, new CurrentSourcesChangedEventArgs(signalType, previousSource, sourceDevice)); } } diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index cdf214bf..46507a98 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -14,14 +14,14 @@ namespace PepperDash.Essentials.Devices.Common.Generic; /// public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort, ICurrentSources { - /// - public Dictionary CurrentSources { get; private set; } + /// + public Dictionary CurrentSources { get; private set; } - /// - public Dictionary CurrentSourceKeys { get; private set; } + /// + public Dictionary CurrentSourceKeys { get; private set; } - /// - public event EventHandler CurrentSourcesChanged; + /// + public event EventHandler CurrentSourcesChanged; /// /// Initializes a new instance of the GenericSink class @@ -36,7 +36,7 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputP InputPorts.Add(inputPort); - CurrentSources = new Dictionary + CurrentSources = new Dictionary { { eRoutingSignalType.Audio, null }, { eRoutingSignalType.Video, null }, @@ -49,36 +49,55 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputP }; } - /// - public void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) - { - foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) - { - var flagValue = Convert.ToInt32(type); - // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). - // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. - if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) - { - this.LogDebug("Skipping {type}", type); - continue; - } + /// + public virtual void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice) + { + foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) + { + var flagValue = Convert.ToInt32(type); + // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). + // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. + if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) + { + this.LogDebug("Skipping {type}", type); + continue; + } - this.LogDebug("setting {type}", type); + this.LogDebug("setting {type}", type); - if (signalType.HasFlag(type)) - { - UpdateCurrentSources(type, sourceListKey, sourceListItem); - } - } - // Raise the CurrentSourcesChanged event - CurrentSourcesChanged?.Invoke(this, EventArgs.Empty); - } + var previousSource = CurrentSources[type]; - private void UpdateCurrentSources(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem) - { - CurrentSources[signalType] = sourceListItem; - CurrentSourceKeys[signalType] = sourceListKey; - } + if (signalType.HasFlag(type)) + { + UpdateCurrentSources(type, previousSource, sourceDevice); + } + } + } + + private void UpdateCurrentSources(eRoutingSignalType signalType, IRoutingSource previousSource, IRoutingSource sourceDevice) + { + if (CurrentSources.ContainsKey(signalType)) + { + CurrentSources[signalType] = sourceDevice; + } + else + { + CurrentSources.Add(signalType, sourceDevice); + } + + // Update the current source key for the specified signal type + if (CurrentSourceKeys.ContainsKey(signalType)) + { + CurrentSourceKeys[signalType] = sourceDevice.Key; + } + else + { + CurrentSourceKeys.Add(signalType, sourceDevice.Key); + } + + // Raise the CurrentSourcesChanged event + CurrentSourcesChanged?.Invoke(this, new CurrentSourcesChangedEventArgs(signalType, previousSource, sourceDevice)); + } /// /// Gets or sets the InputPorts diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs index 2dd2b0d2..54a9d989 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.Routing; using PepperDash.Essentials.Devices.Common.Sources; using Serilog.Events; @@ -22,10 +25,19 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink /// /// The currently active input port, which for this device is always AnyVideoIn - /// This is used by the routing system to determine where to route video sources when this device is a destination + /// This is used by the routing system to determine where to route video sources when this device is a destination /// public RoutingInputPort CurrentInputPort => AnyVideoIn; + /// + public Dictionary CurrentSources { get; private set; } + + /// + public Dictionary CurrentSourceKeys { get; private set; } + + /// + public event EventHandler CurrentSourcesChanged; + #region IRoutingInputs Members /// @@ -47,15 +59,77 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink { (AnyVideoIn = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.None, 0, this)) }; + + CurrentSources = new Dictionary + { + { eRoutingSignalType.Audio, null }, + { eRoutingSignalType.Video, null }, + }; + + CurrentSourceKeys = new Dictionary + { + { eRoutingSignalType.Audio, string.Empty }, + { eRoutingSignalType.Video, string.Empty }, + }; + } + + /// + public virtual void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice) + { + foreach (eRoutingSignalType type in Enum.GetValues(typeof(eRoutingSignalType))) + { + var flagValue = Convert.ToInt32(type); + // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). + // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. + if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) + { + this.LogDebug("Skipping {type}", type); + continue; + } + + this.LogDebug("setting {type}", type); + + var previousSource = CurrentSources[type]; + + if (signalType.HasFlag(type)) + { + UpdateCurrentSources(type, previousSource, sourceDevice); + } + } + } + + private void UpdateCurrentSources(eRoutingSignalType signalType, IRoutingSource previousSource, IRoutingSource sourceDevice) + { + if (CurrentSources.ContainsKey(signalType)) + { + CurrentSources[signalType] = sourceDevice; + } + else + { + CurrentSources.Add(signalType, sourceDevice); + } + + // Update the current source key for the specified signal type + if (CurrentSourceKeys.ContainsKey(signalType)) + { + CurrentSourceKeys[signalType] = sourceDevice.Key; + } + else + { + CurrentSourceKeys.Add(signalType, sourceDevice.Key); + } + + // Raise the CurrentSourcesChanged event + CurrentSourcesChanged?.Invoke(this, new CurrentSourcesChangedEventArgs(signalType, previousSource, sourceDevice)); } #region IRunRouteAction Members /// /// Runs a route action for the specified route key and source list key. Optionally, a callback can be provided to be executed upon successful completion. - /// - /// - /// + /// + /// + /// public void RunRouteAction(string routeKey, string sourceListKey) { RunRouteAction(routeKey, sourceListKey, null); diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs index 942832f3..9cc4c1d8 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs @@ -1,6 +1,9 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; using Serilog.Events; namespace PepperDash.Essentials.Devices.Common.SoftCodec; @@ -27,6 +30,15 @@ public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWi } } + /// + public Dictionary CurrentSources { get; private set; } + + /// + public Dictionary CurrentSourceKeys { get; private set; } + + /// + public event System.EventHandler CurrentSourcesChanged; + /// /// Initializes a new instance of the class /// @@ -49,6 +61,18 @@ public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWi InputPorts.Add(inputPort); } + CurrentSources = new Dictionary + { + { eRoutingSignalType.Audio, null }, + { eRoutingSignalType.Video, null }, + }; + + CurrentSourceKeys = new Dictionary + { + { eRoutingSignalType.Audio, string.Empty }, + { eRoutingSignalType.Video, string.Empty }, + }; + if (!props.HasCameraInputs) { return; @@ -62,6 +86,56 @@ public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWi } } + /// + public virtual void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice) + { + foreach (eRoutingSignalType type in System.Enum.GetValues(typeof(eRoutingSignalType))) + { + var flagValue = System.Convert.ToInt32(type); + // Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag). + // (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set. + if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0) + { + this.LogDebug("Skipping {type}", type); + continue; + } + + this.LogDebug("setting {type}", type); + + var previousSource = CurrentSources[type]; + + if (signalType.HasFlag(type)) + { + UpdateCurrentSources(type, previousSource, sourceDevice); + } + } + } + + private void UpdateCurrentSources(eRoutingSignalType signalType, IRoutingSource previousSource, IRoutingSource sourceDevice) + { + if (CurrentSources.ContainsKey(signalType)) + { + CurrentSources[signalType] = sourceDevice; + } + else + { + CurrentSources.Add(signalType, sourceDevice); + } + + // Update the current source key for the specified signal type + if (CurrentSourceKeys.ContainsKey(signalType)) + { + CurrentSourceKeys[signalType] = sourceDevice.Key; + } + else + { + CurrentSourceKeys.Add(signalType, sourceDevice.Key); + } + + // Raise the CurrentSourcesChanged event + CurrentSourcesChanged?.Invoke(this, new CurrentSourcesChangedEventArgs(signalType, previousSource, sourceDevice)); + } + /// /// Gets or sets the InputPorts /// diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs index a7c4614b..e2c1473d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs @@ -69,7 +69,6 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class CurrentSourcesStateMessage : DeviceStateMessageBase { - /// /// Gets or sets the CurrentSourceKey /// @@ -81,6 +80,6 @@ namespace PepperDash.Essentials.AppServer.Messengers /// Gets or sets the CurrentSource /// [JsonProperty("currentSources")] - public Dictionary CurrentSources { get; set; } + public Dictionary CurrentSources { get; set; } } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs index d5038d91..42c7c9cd 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs @@ -1,7 +1,5 @@ using System; -using Newtonsoft.Json; using PepperDash.Core; -using PepperDash.Core.Logging; using PepperDash.Essentials.Core; @@ -17,29 +15,22 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public IRunRouteAction RoutingDevice { get; private set; } + /// + /// Initializes a new instance of the RunRouteActionMessenger class + /// + /// The key. + /// The routing device. + /// The message path. + /// public RunRouteActionMessenger(string key, IRunRouteAction routingDevice, string messagePath) : base(key, messagePath, routingDevice as IKeyName) { RoutingDevice = routingDevice ?? throw new ArgumentNullException("routingDevice"); - - - if (RoutingDevice is IRoutingSink routingSink) - { - routingSink.CurrentSourceChange += RoutingSink_CurrentSourceChange; - } - } - - private void RoutingSink_CurrentSourceChange(SourceListItem info, ChangeType type) - { - SendRoutingFullMessageObject(); } + /// protected override void RegisterActions() { - AddAction("/fullStatus", (id, content) => SendRoutingFullMessageObject(id)); - - AddAction("/routingStatus", (id, content) => SendRoutingFullMessageObject(id)); - AddAction("/source", (id, content) => { var c = content.ToObject(); @@ -55,41 +46,7 @@ namespace PepperDash.Essentials.AppServer.Messengers RoutingDevice.RunRouteAction(c.SourceListItemKey, sourceListKey); }); - if (RoutingDevice is IRoutingSink sinkDevice) - { - sinkDevice.CurrentSourceChange += (o, a) => SendRoutingFullMessageObject(); - } - } - - /// - /// Helper method to update full status of the routing device - /// - private void SendRoutingFullMessageObject(string id = null) - { - if (RoutingDevice is IRoutingSink sinkDevice) - { - var sourceKey = sinkDevice.CurrentSourceInfoKey; - - if (string.IsNullOrEmpty(sourceKey)) - sourceKey = "none"; - - PostStatusMessage(new RoutingStateMessage - { - SelectedSourceKey = sourceKey - }); - } } } - /// - /// Represents a RoutingStateMessage - /// - public class RoutingStateMessage : DeviceStateMessageBase - { - /// - /// Gets or sets the SelectedSourceKey - /// - [JsonProperty("selectedSourceKey")] - public string SelectedSourceKey { get; set; } - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs index 742251db..c486010d 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -719,29 +719,6 @@ namespace PepperDash.Essentials.RoomBridges } } - if (room is IHasDefaultDisplay defDisplayRoom) - { - this.LogVerbose("Getting default display config"); - configuration.DefaultDisplayKey = defDisplayRoom.DefaultDisplay.Key; - configuration.Destinations.Add(eSourceListItemDestinationTypes.defaultDisplay, defDisplayRoom.DefaultDisplay.Key); - } - - if (room is IHasMultipleDisplays multiDisplayRoom) - { - this.LogVerbose("Getting multiple display config"); - - if (multiDisplayRoom.Displays == null) - { - this.LogVerbose("Displays collection is null"); - } - else - { - this.LogVerbose("Displays collection exists"); - - configuration.Destinations = multiDisplayRoom.Displays.ToDictionary(kv => kv.Key, kv => kv.Value.Key); - } - } - if (room is IHasAccessoryDevices accRoom) { Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Getting accessory devices config", this); @@ -1015,13 +992,6 @@ 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; } - - /// /// Gets or sets the EnvironmentalDevices /// @@ -1106,7 +1076,6 @@ namespace PepperDash.Essentials.RoomBridges /// public RoomConfiguration() { - Destinations = new Dictionary(); EnvironmentalDevices = new List(); SourceList = new Dictionary(); TouchpanelKeys = new List(); From 7076eafc213f9e57d8bdde33a075b7cc39583549 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 30 Mar 2026 11:44:15 -0600 Subject: [PATCH 027/161] Refator: Refactor timer implementation across multiple classes to use System.Timers.Timer instead of CTimer for improved consistency and performance. - Updated RelayControlledShade to utilize Timer for output pulsing. - Refactored MockVC to replace CTimer with Timer for call status simulation. - Modified VideoCodecBase to enhance documentation and improve feedback handling. - Removed obsolete IHasCamerasMessenger and updated related classes to use IHasCamerasWithControls. - Adjusted PressAndHoldHandler to implement Timer for button hold actions. - Enhanced logging throughout MobileControl and RoomBridges for better debugging and information tracking. - Cleaned up unnecessary comments and improved exception handling in various classes. --- .../Comm/GenericSecureTcpIpClient.cs | 57 +- .../GenericSecureTcpIpClient_ForServer.cs | 45 +- .../Comm/GenericSecureTcpIpServer.cs | 51 +- src/PepperDash.Core/Comm/GenericSshClient.cs | 45 +- .../Comm/GenericTcpIpClient_ForServer.cs | 122 +-- .../Comm/GenericTcpIpServer.cs | 47 +- src/PepperDash.Core/Comm/GenericUdpServer.cs | 20 +- .../Config/PortalConfigReader.cs | 1 - .../JsonToSimpl/JsonToSimplChildObjectBase.cs | 27 +- .../JsonToSimpl/JsonToSimplFileMaster.cs | 65 +- .../JsonToSimpl/JsonToSimplGenericMaster.cs | 17 +- .../JsonToSimpl/JsonToSimplMaster.cs | 4 +- .../JsonToSimplPortalFileMaster.cs | 27 +- .../Logging/CrestronEnricher.cs | 8 + src/PepperDash.Core/Logging/Debug.cs | 389 ++++++---- src/PepperDash.Core/Logging/DebugContext.cs | 283 ------- .../Logging/DebugWebsocketSink.cs | 44 +- .../PasswordManagement/PasswordManager.cs | 416 +++++----- .../eSourceListItemDestinationTypes.cs | 94 --- .../Feedbacks/BoolFeedbackOneShot.cs | 22 +- .../Feedbacks/BoolFeedbackPulseExtender.cs | 11 +- .../Fusion/IEssentialsRoomFusionController.cs | 40 +- .../Interfaces/ILogStrings.cs | 19 - .../Interfaces/ILogStringsWithLevel.cs | 19 - .../Monitoring/StatusMonitorBase.cs | 36 +- .../Monitoring/SystemMonitorController.cs | 8 +- .../Ramps and Increments/ActionIncrementer.cs | 34 +- .../UshortSigIncrementer.cs | 59 +- .../Room/Combining/EssentialsRoomCombiner.cs | 19 +- .../EssentialsNDisplayRoomPropertiesConfig.cs | 6 - .../Timers/CountdownTimer.cs | 48 +- .../Timers/RetriggerableTimer.cs | 52 +- .../Keyboards/HabaneroKeyboardController.cs | 129 +++- .../Touchpanels/TriListExtensions.cs | 10 +- .../Cameras/CameraVisca.cs | 715 ------------------ .../Interfaces/CameraSelectedEventArgs.cs | 25 - .../Cameras/Interfaces/IHasCameras.cs | 41 - .../Interfaces/IHasCamerasWithControls.cs | 2 +- .../Cameras/Interfaces/IHasCodecCameras.cs | 2 +- .../Codec/CodecScheduleAwareness.cs | 17 +- .../Displays/BasicIrDisplay.cs | 14 +- .../Displays/DisplayBase.cs | 6 +- .../Displays/MockDisplay.cs | 75 +- .../Shades/RelayControlledShade.cs | 6 +- .../VideoCodec/MockVC/MockVC.cs | 38 +- .../VideoCodec/VideoCodecBase.cs | 46 +- .../Messengers/IHasCamerasMessenger.cs | 100 --- .../Messengers/MessengerBase.cs | 1 - .../Messengers/PressAndHoldHandler.cs | 25 +- .../Messengers/SIMPLDirectRouteMessenger.cs | 4 +- .../Messengers/SIMPLVtcMessenger.cs | 11 +- .../Messengers/SystemMonitorMessenger.cs | 1 - .../Messengers/VideoCodecBaseMessenger.cs | 21 +- .../MobileControlSystemController.cs | 55 +- .../MobileControlSIMPLRoomBridge.cs | 60 +- .../MobileControlWebsocketServer.cs | 1 - 56 files changed, 1343 insertions(+), 2197 deletions(-) delete mode 100644 src/PepperDash.Core/Logging/DebugContext.cs delete mode 100644 src/PepperDash.Essentials.Core/Devices/eSourceListItemDestinationTypes.cs delete mode 100644 src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs delete mode 100644 src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs delete mode 100644 src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs delete mode 100644 src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCameras.cs delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasMessenger.cs diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs index e81494e4..c988db02 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs @@ -3,8 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; +using System.Timers; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -183,7 +182,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging } // private Timer for auto reconnect - private CTimer RetryTimer; + private Timer RetryTimer; #endregion @@ -266,12 +265,12 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging /// public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } - CTimer HeartbeatSendTimer; - CTimer HeartbeatAckTimer; + Timer HeartbeatSendTimer; + Timer HeartbeatAckTimer; // Used to force disconnection on a dead connect attempt - CTimer ConnectFailTimer; - CTimer WaitForSharedKey; + Timer ConnectFailTimer; + Timer WaitForSharedKey; private int ConnectionCount; bool ProgramIsStopping; @@ -493,7 +492,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); - ConnectFailTimer = new CTimer(o => + ConnectFailTimer = new Timer(30000) { AutoReset = false }; + ConnectFailTimer.Elapsed += (s, e) => { this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); if (IsTryingToConnect) @@ -507,7 +507,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging //SecureClient.DisconnectFromServer(); //CheckClosedAndTryReconnect(); } - }, 30000); + }; + ConnectFailTimer.Start(); this.LogVerbose("Making Connection Count:{0}", ConnectionCount); _client.ConnectToServerAsync(o => @@ -528,16 +529,16 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging if (SharedKeyRequired) { WaitingForSharedKeyResponse = true; - WaitForSharedKey = new CTimer(timer => + WaitForSharedKey = new Timer(15000) { AutoReset = false }; + WaitForSharedKey.Elapsed += (s, e) => { - this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); - // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup o.DisconnectFromServer(); //CheckClosedAndTryReconnect(); //OnClientReadyForcommunications(false); // Should send false event - }, 15000); + }; + WaitForSharedKey.Start(); } else { @@ -637,7 +638,9 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging } if (AutoReconnectTriggered != null) AutoReconnectTriggered(this, new EventArgs()); - RetryTimer = new CTimer(o => Connect(), rndTime); + RetryTimer = new Timer(rndTime) { AutoReset = false }; + RetryTimer.Elapsed += (s, e) => Connect(); + RetryTimer.Start(); } } @@ -698,11 +701,11 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. if (handler != null) { - if (Monitor.TryEnter(_dequeueLock)) - Task.Run(() => DequeueEvent()); + if (System.Threading.Monitor.TryEnter(_dequeueLock)) + System.Threading.Tasks.Task.Run(() => DequeueEvent()); } - } - else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync + } + else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync { client.DisconnectFromServer(); } @@ -732,7 +735,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging this.LogError(e, "DequeueEvent error: {0}", e.Message); } // Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it. - Monitor.Exit(_dequeueLock); + System.Threading.Monitor.Exit(_dequeueLock); } void HeartbeatStart() @@ -743,11 +746,15 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging if (HeartbeatSendTimer == null) { - HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true }; + HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null); + HeartbeatSendTimer.Start(); } if (HeartbeatAckTimer == null) { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true }; + HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null); + HeartbeatAckTimer.Start(); } } @@ -791,11 +798,15 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging { if (HeartbeatAckTimer != null) { - HeartbeatAckTimer.Reset(HeartbeatInterval * 2); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer.Interval = HeartbeatInterval * 2; + HeartbeatAckTimer.Start(); } else { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true }; + HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null); + HeartbeatAckTimer.Start(); } this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString); return remainingText; diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs index 42b35815..84af5a47 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs @@ -17,6 +17,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using System.Timers; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -223,7 +224,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect /// /// private Timer for auto reconnect /// - CTimer RetryTimer; + System.Timers.Timer RetryTimer; /// @@ -255,13 +256,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect /// public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } - CTimer HeartbeatSendTimer; - CTimer HeartbeatAckTimer; + System.Timers.Timer HeartbeatSendTimer; + System.Timers.Timer HeartbeatAckTimer; /// /// Used to force disconnection on a dead connect attempt /// - CTimer ConnectFailTimer; - CTimer WaitForSharedKey; + System.Timers.Timer ConnectFailTimer; + System.Timers.Timer WaitForSharedKey; private int ConnectionCount; /// /// Internal secure client @@ -457,7 +458,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); - ConnectFailTimer = new CTimer(o => + ConnectFailTimer = new System.Timers.Timer(30000) { AutoReset = false }; + ConnectFailTimer.Elapsed += (s, e) => { this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); if (IsTryingToConnect) @@ -471,7 +473,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect //SecureClient.DisconnectFromServer(); //CheckClosedAndTryReconnect(); } - }, 30000); + }; + ConnectFailTimer.Start(); this.LogVerbose("Making Connection Count:{0}", ConnectionCount); Client.ConnectToServerAsync(o => @@ -492,16 +495,16 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect if (SharedKeyRequired) { WaitingForSharedKeyResponse = true; - WaitForSharedKey = new CTimer(timer => + WaitForSharedKey = new System.Timers.Timer(15000) { AutoReset = false }; + WaitForSharedKey.Elapsed += (s, e) => { - this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); - // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup o.DisconnectFromServer(); //CheckClosedAndTryReconnect(); //OnClientReadyForcommunications(false); // Should send false event - }, 15000); + }; + WaitForSharedKey.Start(); } else { @@ -596,7 +599,9 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect } if (AutoReconnectTriggered != null) AutoReconnectTriggered(this, new EventArgs()); - RetryTimer = new CTimer(o => Connect(), rndTime); + RetryTimer = new System.Timers.Timer(rndTime) { AutoReset = false }; + RetryTimer.Elapsed += (s, e) => Connect(); + RetryTimer.Start(); } } @@ -702,11 +707,15 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect if (HeartbeatSendTimer == null) { - HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + HeartbeatSendTimer = new System.Timers.Timer(HeartbeatInterval) { AutoReset = true }; + HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null); + HeartbeatSendTimer.Start(); } if (HeartbeatAckTimer == null) { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true }; + HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null); + HeartbeatAckTimer.Start(); } } @@ -750,11 +759,15 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect { if (HeartbeatAckTimer != null) { - HeartbeatAckTimer.Reset(HeartbeatInterval * 2); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer.Interval = HeartbeatInterval * 2; + HeartbeatAckTimer.Start(); } else { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true }; + HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null); + HeartbeatAckTimer.Start(); } this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString); return remainingText; diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs index 673afcbd..fafafbdc 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs @@ -2,8 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; +using System.Timers; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -92,7 +91,7 @@ public class GenericSecureTcpIpServer : Device /// /// Timer to operate the bandaid monitor client in a loop. /// - CTimer MonitorClientTimer; + Timer MonitorClientTimer; /// /// @@ -259,7 +258,7 @@ public class GenericSecureTcpIpServer : Device public string HeartbeatStringToMatch { get; set; } //private timers for Heartbeats per client - Dictionary HeartbeatTimerDictionary = new Dictionary(); + Dictionary HeartbeatTimerDictionary = new Dictionary(); //flags to show the secure server is waiting for client at index to send the shared key List WaitingForSharedKey = new List(); @@ -592,11 +591,17 @@ public class GenericSecureTcpIpServer : Device if (noDelimiter.Contains(HeartbeatStringToMatch)) { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) - HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); + { + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs; + HeartbeatTimerDictionary[clientIndex].Start(); + } else { - CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); - HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false }; + heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex); + heartbeatTimer.Start(); + HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer); } this.LogDebug("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); // Return Heartbeat @@ -607,11 +612,17 @@ public class GenericSecureTcpIpServer : Device else { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) - HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); + { + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs; + HeartbeatTimerDictionary[clientIndex].Start(); + } else { - CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); - HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false }; + heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex); + heartbeatTimer.Start(); + HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer); } this.LogInformation("Heartbeat Received: {0}, from client index: {1}", received, clientIndex); } @@ -665,7 +676,6 @@ public class GenericSecureTcpIpServer : Device SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); var discoResult = SecureServer.Disconnect(clientIndex); - //Debug.Console(1, this, "{0}", discoResult); if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) { @@ -694,8 +704,6 @@ public class GenericSecureTcpIpServer : Device try { - - // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { this.LogInformation("SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber); @@ -725,7 +733,7 @@ public class GenericSecureTcpIpServer : Device //Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state //after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag //is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event. - Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex))); + System.Threading.Tasks.Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex))); } #endregion @@ -770,7 +778,10 @@ public class GenericSecureTcpIpServer : Device { if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) { - HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); + var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false }; + heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex); + heartbeatTimer.Start(); + HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer); } } @@ -860,8 +871,8 @@ public class GenericSecureTcpIpServer : Device //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. if (handler != null) { - if (Monitor.TryEnter(_dequeueLock)) - Task.Run(() => DequeueEvent()); + if (System.Threading.Monitor.TryEnter(_dequeueLock)) + System.Threading.Tasks.Task.Run(() => DequeueEvent()); } } else @@ -894,7 +905,7 @@ public class GenericSecureTcpIpServer : Device this.LogError(e, "DequeueEvent error"); } // Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it. - Monitor.Exit(_dequeueLock); + System.Threading.Monitor.Exit(_dequeueLock); } #endregion @@ -991,7 +1002,9 @@ public class GenericSecureTcpIpServer : Device { return; } - MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); + MonitorClientTimer = new Timer(60000) { AutoReset = false }; + MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient(); + MonitorClientTimer.Start(); } /// diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index 0bef3f74..9d7fe001 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; using System.Text; -using System.Threading; -using System.Threading.Tasks; +using System.Timers; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; -using Org.BouncyCastle.Utilities; using PepperDash.Core.Logging; using Renci.SshNet; using Renci.SshNet.Common; @@ -134,9 +131,9 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR ShellStream TheStream; - CTimer ReconnectTimer; + Timer ReconnectTimer; - private SemaphoreSlim connectLock = new SemaphoreSlim(1); + private System.Threading.SemaphoreSlim connectLock = new System.Threading.SemaphoreSlim(1); private bool DisconnectLogged = false; @@ -155,13 +152,14 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR Password = password; AutoReconnectIntervalMs = 5000; - ReconnectTimer = new CTimer(o => + ReconnectTimer = new Timer { AutoReset = false, Enabled = false }; + ReconnectTimer.Elapsed += (s, e) => { if (ConnectEnabled) { Connect(); } - }, System.Threading.Timeout.Infinite); + }; } /// @@ -173,13 +171,14 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; - ReconnectTimer = new CTimer(o => + ReconnectTimer = new Timer { AutoReset = false, Enabled = false }; + ReconnectTimer.Elapsed += (s, e) => { if (ConnectEnabled) { Connect(); } - }, System.Threading.Timeout.Infinite); + }; } /// @@ -265,7 +264,6 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR catch (SshConnectionException e) { var ie = e.InnerException; // The details are inside!! - var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; if (ie is SocketException) { @@ -289,7 +287,9 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + ReconnectTimer.Stop(); + ReconnectTimer.Interval = AutoReconnectIntervalMs; + ReconnectTimer.Start(); } } catch (SshOperationTimeoutException ex) @@ -301,19 +301,22 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + ReconnectTimer.Stop(); + ReconnectTimer.Interval = AutoReconnectIntervalMs; + ReconnectTimer.Start(); } } catch (Exception e) { - var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; this.LogException(e, "Unhandled exception on connect"); DisconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + ReconnectTimer.Stop(); + ReconnectTimer.Interval = AutoReconnectIntervalMs; + ReconnectTimer.Start(); } } } @@ -434,7 +437,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR /// void Client_ErrorOccurred(object sender, ExceptionEventArgs e) { - Task.Run(() => + System.Threading.Tasks.Task.Run(() => { if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) this.LogError("Disconnected by remote"); @@ -452,7 +455,9 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR if (AutoReconnect && ConnectEnabled) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + ReconnectTimer.Stop(); + ReconnectTimer.Interval = AutoReconnectIntervalMs; + ReconnectTimer.Start(); } }); } @@ -497,7 +502,8 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - ReconnectTimer.Reset(); + ReconnectTimer.Stop(); + ReconnectTimer.Start(); } catch (Exception ex) { @@ -531,7 +537,8 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - ReconnectTimer.Reset(); + ReconnectTimer.Stop(); + ReconnectTimer.Start(); } catch (Exception ex) { diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs index a1a0887f..08259f6e 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs @@ -11,10 +11,9 @@ PepperDash Technology Corporation reserves all rights under applicable laws. ------------------------------------ */ using System; -using System.Collections.Generic; using System.Linq; using System.Text; -using System.Text.RegularExpressions; +using System.Timers; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -210,7 +209,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect /// /// private Timer for auto reconnect /// - CTimer RetryTimer; + Timer RetryTimer; /// @@ -237,13 +236,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect /// public int HeartbeatInterval = 50000; - CTimer HeartbeatSendTimer; - CTimer HeartbeatAckTimer; + Timer HeartbeatSendTimer; + Timer HeartbeatAckTimer; /// /// Used to force disconnection on a dead connect attempt /// - CTimer ConnectFailTimer; - CTimer WaitForSharedKey; + Timer ConnectFailTimer; + Timer WaitForSharedKey; private int ConnectionCount; /// /// Internal secure client @@ -303,7 +302,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect { if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection"); + this.LogInformation("Program stopping. Closing Client connection"); ProgramIsStopping = true; Disconnect(); } @@ -316,17 +315,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect public void Connect() { ConnectionCount++; - Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); + this.LogDebug("Attempting connect Count:{0}", ConnectionCount); if (IsConnected) { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); + this.LogInformation("Already connected. Ignoring."); return; } if (IsTryingToConnect) { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); + this.LogInformation("Already trying to connect. Ignoring."); return; } try @@ -339,17 +338,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect } if (string.IsNullOrEmpty(Hostname)) { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); + this.LogWarning("DynamicTcpClient: No address set"); return; } if (Port < 1 || Port > 65535) { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); + this.LogWarning("DynamicTcpClient: Invalid port"); return; } if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); + this.LogWarning("DynamicTcpClient: No Shared Key set"); return; } @@ -370,9 +369,10 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); - ConnectFailTimer = new CTimer(o => + ConnectFailTimer = new Timer(30000) { AutoReset = false }; + ConnectFailTimer.Elapsed += (s, e) => { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); + this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); if (IsTryingToConnect) { IsTryingToConnect = false; @@ -384,12 +384,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect //SecureClient.DisconnectFromServer(); //CheckClosedAndTryReconnect(); } - }, 30000); + }; + ConnectFailTimer.Start(); - Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); + this.LogDebug("Making Connection Count:{0}", ConnectionCount); Client.ConnectToServerAsync(o => { - Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); + this.LogDebug("ConnectToServerAsync Count:{0} Ran!", ConnectionCount); if (ConnectFailTimer != null) { @@ -399,22 +400,22 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) { - Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); + this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); o.ReceiveDataAsync(Receive); if (SharedKeyRequired) { WaitingForSharedKeyResponse = true; - WaitForSharedKey = new CTimer(timer => + WaitForSharedKey = new Timer(15000) { AutoReset = false }; + WaitForSharedKey.Elapsed += (s, e) => { - - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); - // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); + this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup o.DisconnectFromServer(); //CheckClosedAndTryReconnect(); //OnClientReadyForcommunications(false); // Should send false event - }, 15000); + }; + WaitForSharedKey.Start(); } else { @@ -428,14 +429,15 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect } else { - Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); + this.LogWarning("Connect attempt failed {0}", o.ClientStatus); CheckClosedAndTryReconnect(); } }); } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message); + this.LogException(ex, "Client connection exception: {0}", ex.Message); + this.LogVerbose("Stack Trace: {0}", ex.StackTrace); IsTryingToConnect = false; CheckClosedAndTryReconnect(); } @@ -472,7 +474,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect if (Client != null) { //SecureClient.DisconnectFromServer(); - Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); + this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); Client.SocketStatusChange -= Client_SocketStatusChange; Client.Dispose(); Client = null; @@ -494,20 +496,22 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect { if (Client != null) { - Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); + this.LogVerbose("Cleaning up remotely closed/failed connection."); Cleanup(); } if (!DisconnectCalledByUser && AutoReconnect) { var halfInterval = AutoReconnectIntervalMs / 2; var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; - Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); + this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime); if (RetryTimer != null) { RetryTimer.Stop(); RetryTimer = null; } - RetryTimer = new CTimer(o => Connect(), rndTime); + RetryTimer = new Timer(rndTime) { AutoReset = false }; + RetryTimer.Elapsed += (s, e) => Connect(); + RetryTimer.Start(); } } @@ -526,18 +530,18 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect { var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); + this.LogVerbose("Client Received:\r--------\r{0}\r--------", str); if (!string.IsNullOrEmpty(checkHeartbeat(str))) { if (SharedKeyRequired && str == "SharedKey:") { - Debug.Console(2, this, "Server asking for shared key, sending"); + this.LogVerbose("Server asking for shared key, sending"); SendText(SharedKey + "\n"); } else if (SharedKeyRequired && str == "Shared Key Match") { StopWaitForSharedKeyTimer(); - Debug.Console(2, this, "Shared key confirmed. Ready for communication"); + this.LogVerbose("Shared key confirmed. Ready for communication"); OnClientReadyForcommunications(true); // Successful key exchange } else @@ -553,7 +557,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect } catch (Exception ex) { - Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); + this.LogException(ex, "Error receiving data: {1}. Error: {0}", ex.Message, str); + this.LogVerbose("Stack Trace: {0}", ex.StackTrace); } } if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) @@ -564,15 +569,19 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect { if (HeartbeatEnabled) { - Debug.Console(2, this, "Starting Heartbeat"); + this.LogVerbose("Starting Heartbeat"); if (HeartbeatSendTimer == null) { - HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true }; + HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null); + HeartbeatSendTimer.Start(); } if (HeartbeatAckTimer == null) { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true }; + HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null); + HeartbeatAckTimer.Start(); } } @@ -582,13 +591,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect if (HeartbeatSendTimer != null) { - Debug.Console(2, this, "Stoping Heartbeat Send"); + this.LogVerbose("Stoping Heartbeat Send"); HeartbeatSendTimer.Stop(); HeartbeatSendTimer = null; } if (HeartbeatAckTimer != null) { - Debug.Console(2, this, "Stoping Heartbeat Ack"); + this.LogVerbose("Stoping Heartbeat Ack"); HeartbeatAckTimer.Stop(); HeartbeatAckTimer = null; } @@ -597,7 +606,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect void SendHeartbeat(object notused) { this.SendText(HeartbeatString); - Debug.Console(2, this, "Sending Heartbeat"); + this.LogVerbose("Sending Heartbeat"); } @@ -616,13 +625,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect { if (HeartbeatAckTimer != null) { - HeartbeatAckTimer.Reset(HeartbeatInterval * 2); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer.Interval = HeartbeatInterval * 2; + HeartbeatAckTimer.Start(); } else { - HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true }; + HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null); + HeartbeatAckTimer.Start(); } - Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); + this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString); return remainingText; } } @@ -630,7 +643,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect } catch (Exception ex) { - Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + this.LogException(ex, "Error checking heartbeat: {0}", ex.Message); + this.LogVerbose("Stack Trace: {0}", ex.StackTrace); } return received; } @@ -644,7 +658,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect if (IsConnected) { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); + this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); SendText("Heartbeat not received by server, closing connection"); CheckClosedAndTryReconnect(); } @@ -652,7 +666,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect } catch (Exception ex) { - ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); + this.LogException(ex, "Heartbeat timeout Error on Client: {0}, {1}", Key, ex.Message); + this.LogVerbose("Stack Trace: {0}", ex.StackTrace); } } @@ -685,14 +700,15 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? if (n <= 0) { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); + this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key); } }); } } catch (Exception ex) { - Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); + this.LogException(ex, "Error sending text: {1}. Error: {0}", ex.Message, text); + this.LogVerbose("Stack Trace: {0}", ex.StackTrace); } } } @@ -711,7 +727,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect } catch (Exception ex) { - Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); + this.LogException(ex, "Error sending bytes. Error: {0}", ex.Message); + this.LogVerbose("Stack Trace: {0}", ex.StackTrace); } } } @@ -730,7 +747,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect } try { - Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); + this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); OnConnectionChange(); @@ -744,7 +761,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect } catch (Exception ex) { - Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); + this.LogException(ex, "Error in socket status change callback. Error: {0}", ex.Message); + this.LogVerbose("Stack Trace: {0}", ex.StackTrace); } } diff --git a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs index 41c19504..3d5d5a0c 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs @@ -13,7 +13,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading; +using System.Timers; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using PepperDash.Core.Logging; @@ -80,7 +80,7 @@ public class GenericTcpIpServer : Device /// /// Timer to operate the bandaid monitor client in a loop. /// - CTimer MonitorClientTimer; + Timer MonitorClientTimer; /// /// @@ -250,7 +250,7 @@ public class GenericTcpIpServer : Device public string HeartbeatStringToMatch { get; set; } //private timers for Heartbeats per client - Dictionary HeartbeatTimerDictionary = new Dictionary(); + Dictionary HeartbeatTimerDictionary = new Dictionary(); //flags to show the secure server is waiting for client at index to send the shared key List WaitingForSharedKey = new List(); @@ -577,11 +577,17 @@ public class GenericTcpIpServer : Device if (noDelimiter.Contains(HeartbeatStringToMatch)) { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) - HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); + { + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs; + HeartbeatTimerDictionary[clientIndex].Start(); + } else { - CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); - HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false }; + heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex); + heartbeatTimer.Start(); + HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer); } this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); // Return Heartbeat @@ -592,11 +598,17 @@ public class GenericTcpIpServer : Device else { if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) - HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); + { + HeartbeatTimerDictionary[clientIndex].Stop(); + HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs; + HeartbeatTimerDictionary[clientIndex].Start(); + } else { - CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); - HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); + var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false }; + heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex); + heartbeatTimer.Start(); + HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer); } this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", received, clientIndex); } @@ -650,7 +662,6 @@ public class GenericTcpIpServer : Device SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); var discoResult = myTcpServer.Disconnect(clientIndex); - //Debug.Console(1, this, "{0}", discoResult); if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) { @@ -661,7 +672,8 @@ public class GenericTcpIpServer : Device } catch (Exception ex) { - ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key); + this.LogException(ex, "Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message); + this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace); } } @@ -746,7 +758,10 @@ public class GenericTcpIpServer : Device { if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) { - HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); + var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false }; + heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex); + heartbeatTimer.Start(); + HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer); } } @@ -765,9 +780,9 @@ public class GenericTcpIpServer : Device } catch (Exception ex) { - this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex); + this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message); + this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace); } - //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", // server.State, // MaxClients, // ServerStopped); @@ -929,7 +944,9 @@ public class GenericTcpIpServer : Device { return; } - MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); + MonitorClientTimer = new Timer(60000) { AutoReset = false }; + MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient(); + MonitorClientTimer.Start(); } /// diff --git a/src/PepperDash.Core/Comm/GenericUdpServer.cs b/src/PepperDash.Core/Comm/GenericUdpServer.cs index 6a761e6c..2d18d741 100644 --- a/src/PepperDash.Core/Comm/GenericUdpServer.cs +++ b/src/PepperDash.Core/Comm/GenericUdpServer.cs @@ -183,7 +183,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging if (programEventType != eProgramStatusEventType.Stopping) return; - Debug.Console(1, this, "Program stopping. Disabling Server"); + this.LogInformation("Program stopping. Disabling Server"); Disconnect(); } @@ -199,20 +199,20 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging if (string.IsNullOrEmpty(Hostname)) { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key); + this.LogWarning("GenericUdpServer '{0}': No address set", Key); return; } if (Port < 1 || Port > 65535) { { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key); + this.LogWarning("GenericUdpServer '{0}': Invalid port", Key); return; } } var status = Server.EnableUDPServer(Hostname, Port); - Debug.Console(2, this, "SocketErrorCode: {0}", status); + this.LogVerbose("SocketErrorCode: {0}", status); if (status == SocketErrorCodes.SOCKET_OK) IsConnected = true; @@ -247,7 +247,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging /// void Receive(UDPServer server, int numBytes) { - Debug.Console(2, this, "Received {0} bytes", numBytes); + this.LogVerbose("Received {0} bytes", numBytes); try { @@ -263,13 +263,13 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging if (dataRecivedExtra != null) dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes)); - Debug.Console(2, this, "Bytes: {0}", bytes.ToString()); + this.LogVerbose("Bytes: {0}", bytes.ToString()); var bytesHandler = BytesReceived; if (bytesHandler != null) { if (StreamDebugging.RxStreamDebuggingIsEnabled) { - Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); + this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); } bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } @@ -277,7 +277,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging if (textHandler != null) { if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); + this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); textHandler(this, new GenericCommMethodReceiveTextArgs(str)); } } @@ -302,7 +302,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging if (IsConnected && Server != null) { if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); + this.LogVerbose("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); Server.SendData(bytes, bytes.Length); } @@ -315,7 +315,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging public void SendBytes(byte[] bytes) { if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); if (IsConnected && Server != null) Server.SendData(bytes, bytes.Length); diff --git a/src/PepperDash.Core/Config/PortalConfigReader.cs b/src/PepperDash.Core/Config/PortalConfigReader.cs index 270e10ec..1e24658c 100644 --- a/src/PepperDash.Core/Config/PortalConfigReader.cs +++ b/src/PepperDash.Core/Config/PortalConfigReader.cs @@ -155,7 +155,6 @@ namespace PepperDash.Core.Config; else merged.Add(global, template[global]); - //Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged); return merged; } diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs index 2c1afd06..5e644483 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using PepperDash.Core.Logging; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; namespace PepperDash.Core.JsonToSimpl; @@ -91,7 +92,7 @@ namespace PepperDash.Core.JsonToSimpl; if (Master != null) Master.AddChild(this); else - Debug.Console(1, "JSON Child [{0}] cannot link to master {1}", key, masterUniqueId); + this.LogWarning("JSON Child [{0}] cannot link to master {1}", key, masterUniqueId); } /// @@ -107,7 +108,7 @@ namespace PepperDash.Core.JsonToSimpl; /// public void SetBoolPath(ushort index, string path) { - Debug.Console(1, "JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path); + this.LogDebug("JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path); if (path == null || path.Trim() == string.Empty) return; BoolPaths[index] = path; } @@ -117,7 +118,7 @@ namespace PepperDash.Core.JsonToSimpl; /// public void SetUshortPath(ushort index, string path) { - Debug.Console(1, "JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path); + this.LogDebug("JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path); if (path == null || path.Trim() == string.Empty) return; UshortPaths[index] = path; } @@ -127,7 +128,7 @@ namespace PepperDash.Core.JsonToSimpl; /// public void SetStringPath(ushort index, string path) { - Debug.Console(1, "JSON Child[{0}] SetStringPath {1}={2}", Key, index, path); + this.LogDebug("JSON Child[{0}] SetStringPath {1}={2}", Key, index, path); if (path == null || path.Trim() == string.Empty) return; StringPaths[index] = path; } @@ -140,13 +141,13 @@ namespace PepperDash.Core.JsonToSimpl; { if (!LinkedToObject) { - Debug.Console(1, this, "Not linked to object in file. Skipping"); + this.LogDebug("Not linked to object in file. Skipping"); return; } if (SetAllPathsDelegate == null) { - Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring ProcessAll"); + this.LogDebug("No SetAllPathsDelegate set. Ignoring ProcessAll"); return; } SetAllPathsDelegate(); @@ -206,11 +207,11 @@ namespace PepperDash.Core.JsonToSimpl; bool Process(string path, out string response) { path = GetFullPath(path); - Debug.Console(1, "JSON Child[{0}] Processing {1}", Key, path); + this.LogDebug("JSON Child[{0}] Processing {1}", Key, path); response = ""; if (Master == null) { - Debug.Console(1, "JSONChild[{0}] cannot process without Master attached", Key); + this.LogWarning("JSONChild[{0}] cannot process without Master attached", Key); return false; } @@ -233,7 +234,7 @@ namespace PepperDash.Core.JsonToSimpl; response = (t.HasValues ? t.Children().Count() : 0).ToString(); else response = (string)t; - Debug.Console(1, " ='{0}'", response); + this.LogDebug(" ='{0}'", response); return true; } } @@ -259,13 +260,13 @@ namespace PepperDash.Core.JsonToSimpl; { if (!LinkedToObject) { - Debug.Console(1, this, "Not linked to object in file. Skipping"); + this.LogDebug("Not linked to object in file. Skipping"); return; } if (SetAllPathsDelegate == null) { - Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster"); + this.LogDebug("No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster"); return; } SetAllPathsDelegate(); @@ -327,7 +328,7 @@ namespace PepperDash.Core.JsonToSimpl; var path = GetFullPath(keyPath); try { - Debug.Console(1, "JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave); + this.LogDebug("JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave); //var token = Master.JsonObject.SelectToken(path); //if (token != null) // The path exists in the file @@ -335,7 +336,7 @@ namespace PepperDash.Core.JsonToSimpl; } catch (Exception e) { - Debug.Console(1, "JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e); + this.LogDebug("JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e); } } diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs index a9fa03cd..1cf9efcf 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.RegularExpressions; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; +using PepperDash.Core.Logging; using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; @@ -129,7 +130,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster var fileName = Path.GetFileName(Filepath); OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "Checking '{0}' for '{1}'", fileDirectory, fileName); + this.LogInformation("Checking '{0}' for '{1}'", fileDirectory, fileName); if (Directory.Exists(fileDirectory)) { @@ -143,7 +144,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster var msg = string.Format("JSON file not found: {0}", Filepath); OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange); CrestronConsole.PrintLine(msg); - ErrorLog.Error(msg); + this.LogError(msg); return; } @@ -152,18 +153,18 @@ public class JsonToSimplFileMaster : JsonToSimplMaster ActualFilePath = actualFile.FullName; OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange); OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "Actual JSON file is {0}", ActualFilePath); + this.LogInformation("Actual JSON file is {0}", ActualFilePath); Filename = actualFile.Name; OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange); OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "JSON Filename is {0}", Filename); + this.LogInformation("JSON Filename is {0}", Filename); FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator); OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange); OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "JSON File Path is {0}", FilePathName); + this.LogInformation("JSON File Path is {0}", FilePathName); var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII); @@ -176,7 +177,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster else { OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange); - Debug.Console(1, "'{0}' not found", fileDirectory); + this.LogError("'{0}' not found", fileDirectory); } } catch (Exception e) @@ -184,12 +185,12 @@ public class JsonToSimplFileMaster : JsonToSimplMaster var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message); OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange); CrestronConsole.PrintLine(msg); - ErrorLog.Error(msg); + this.LogException(e, "EvaluateFile Exception: {0}", e.Message); var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace); OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange); CrestronConsole.PrintLine(stackTrace); - ErrorLog.Error(stackTrace); + this.LogVerbose("EvaluateFile: Stack Trace\r{0}", e.StackTrace); } } @@ -213,63 +214,31 @@ public class JsonToSimplFileMaster : JsonToSimplMaster // Make each child update their values into master object foreach (var child in Children) { - Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key); + this.LogInformation("Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key); child.UpdateInputsForMaster(); } if (UnsavedValues == null || UnsavedValues.Count == 0) { - Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID); + this.LogInformation("Master [{0}] No updated values to save. Skipping", UniqueID); return; } lock (FileLock) { - Debug.Console(1, "Saving"); + this.LogInformation("Saving"); foreach (var path in UnsavedValues.Keys) { var tokenToReplace = JsonObject.SelectToken(path); if (tokenToReplace != null) {// It's found tokenToReplace.Replace(UnsavedValues[path]); - Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path); + this.LogInformation("JSON Master[{0}] Updating '{1}'", UniqueID, path); } else // No token. Let's make one { //http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net - Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path); + this.LogWarning("JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path); - // JContainer jpart = JsonObject; - // // walk down the path and find where it goes - //#warning Does not handle arrays. - // foreach (var part in path.Split('.')) - // { - - // var openPos = part.IndexOf('['); - // if (openPos > -1) - // { - // openPos++; // move to number - // var closePos = part.IndexOf(']'); - // var arrayName = part.Substring(0, openPos - 1); // get the name - // var index = Convert.ToInt32(part.Substring(openPos, closePos - openPos)); - - // // Check if the array itself exists and add the item if so - // if (jpart[arrayName] != null) - // { - // var arrayObj = jpart[arrayName] as JArray; - // var item = arrayObj[index]; - // if (item == null) - // arrayObj.Add(new JObject()); - // } - - // Debug.Console(0, "IGNORING MISSING ARRAY VALUE FOR NOW"); - // continue; - // } - // // Build the - // if (jpart[part] == null) - // jpart.Add(new JProperty(part, new JObject())); - // jpart = jpart[part] as JContainer; - // } - // jpart.Replace(UnsavedValues[path]); } } using (StreamWriter sw = new StreamWriter(ActualFilePath)) @@ -282,11 +251,13 @@ public class JsonToSimplFileMaster : JsonToSimplMaster catch (Exception e) { string err = string.Format("Error writing JSON file:\r{0}", e); - Debug.Console(0, err); - ErrorLog.Warn(err); + this.LogException(e, "Error writing JSON file: {0}", e.Message); + this.LogVerbose("Stack Trace:\r{0}", e.StackTrace); return; } } } } } + + diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs index 151773d9..423c435a 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using Crestron.SimplSharp; +using PepperDash.Core.Logging; +using Renci.SshNet.Messages; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; @@ -74,7 +76,8 @@ namespace PepperDash.Core.JsonToSimpl; } catch (Exception e) { - Debug.Console(0, this, "JSON parsing failed:\r{0}", e); + this.LogException(e, "JSON parsing failed:\r{0}", e.Message); + this.LogVerbose("Stack Trace:\r{0}", e.StackTrace); } } @@ -89,36 +92,36 @@ namespace PepperDash.Core.JsonToSimpl; // Make each child update their values into master object foreach (var child in Children) { - Debug.Console(1, this, "Master. checking child [{0}] for updates to save", child.Key); + this.LogDebug("Master. checking child [{0}] for updates to save", child.Key); child.UpdateInputsForMaster(); } if (UnsavedValues == null || UnsavedValues.Count == 0) { - Debug.Console(1, this, "Master. No updated values to save. Skipping"); + this.LogDebug("Master. No updated values to save. Skipping"); return; } lock (WriteLock) { - Debug.Console(1, this, "Saving"); + this.LogDebug("Saving"); foreach (var path in UnsavedValues.Keys) { var tokenToReplace = JsonObject.SelectToken(path); if (tokenToReplace != null) {// It's found tokenToReplace.Replace(UnsavedValues[path]); - Debug.Console(1, this, "Master Updating '{0}'", path); + this.LogDebug("Master Updating '{0}'", path); } else // No token. Let's make one { - Debug.Console(1, "Master Cannot write value onto missing property: '{0}'", path); + this.LogDebug("Master Cannot write value onto missing property: '{0}'", path); } } } if (SaveCallback != null) SaveCallback(JsonObject.ToString()); else - Debug.Console(0, this, "WARNING: No save callback defined."); + this.LogDebug("WARNING: No save callback defined."); } } \ No newline at end of file diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs index 2eb2a1d7..2b2a504f 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs @@ -10,6 +10,7 @@ using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; using JsonSerializationException = NewtonsoftJson::Newtonsoft.Json.JsonSerializationException; using JsonTextReader = NewtonsoftJson::Newtonsoft.Json.JsonTextReader; +using PepperDash.Core.Logging; namespace PepperDash.Core.JsonToSimpl; @@ -142,11 +143,10 @@ namespace PepperDash.Core.JsonToSimpl; { if (UnsavedValues.ContainsKey(path)) { - Debug.Console(0, "Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path); + this.LogWarning("Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path); } else UnsavedValues.Add(path, value); - //Debug.Console(0, "Master[{0}] Unsaved size={1}", UniqueID, UnsavedValues.Count); } /// diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs index caba1c9b..bbd8df2c 100644 --- a/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs +++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs @@ -8,6 +8,7 @@ using Crestron.SimplSharp.CrestronIO; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue; using PepperDash.Core.Config; +using PepperDash.Core.Logging; namespace PepperDash.Core.JsonToSimpl; @@ -61,7 +62,7 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster // If the portal file is xyz.json, then // the file we want to check for first will be called xyz.local.json var localFilepath = Path.ChangeExtension(PortalFilepath, "local.json"); - Debug.Console(0, this, "Checking for local file {0}", localFilepath); + this.LogInformation("Checking for local file {0}", localFilepath); var actualLocalFile = GetActualFileInfoFromPath(localFilepath); if (actualLocalFile != null) @@ -73,7 +74,7 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster // and create the local. else { - Debug.Console(1, this, "Local JSON file not found {0}\rLoading portal JSON file", localFilepath); + this.LogInformation("Local JSON file not found {0}\rLoading portal JSON file", localFilepath); var actualPortalFile = GetActualFileInfoFromPath(portalFilepath); if (actualPortalFile != null) { @@ -86,14 +87,13 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster else { var msg = string.Format("Portal JSON file not found: {0}", PortalFilepath); - Debug.Console(1, this, msg); - ErrorLog.Error(msg); + this.LogError(msg); return; } } // At this point we should have a local file. Do it. - Debug.Console(1, "Reading local JSON file {0}", ActualFilePath); + this.LogInformation("Reading local JSON file {0}", ActualFilePath); string json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII); @@ -107,8 +107,7 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster catch (Exception e) { var msg = string.Format("JSON parsing failed:\r{0}", e); - CrestronConsole.PrintLine(msg); - ErrorLog.Error(msg); + this.LogError(msg); return; } } @@ -149,30 +148,30 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster // Make each child update their values into master object foreach (var child in Children) { - Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key); + this.LogInformation("Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key); child.UpdateInputsForMaster(); } if (UnsavedValues == null || UnsavedValues.Count == 0) { - Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID); + this.LogInformation("Master [{0}] No updated values to save. Skipping", UniqueID); return; } lock (FileLock) { - Debug.Console(1, "Saving"); + this.LogInformation("Saving"); foreach (var path in UnsavedValues.Keys) { var tokenToReplace = JsonObject.SelectToken(path); if (tokenToReplace != null) {// It's found tokenToReplace.Replace(UnsavedValues[path]); - Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path); + this.LogInformation("JSON Master[{0}] Updating '{1}'", UniqueID, path); } else // No token. Let's make one { //http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net - Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path); + this.LogWarning("JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path); } } @@ -186,8 +185,8 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster catch (Exception e) { string err = string.Format("Error writing JSON file:\r{0}", e); - Debug.Console(0, err); - ErrorLog.Warn(err); + this.LogException(e, "Error writing JSON file: {0}", e.Message); + this.LogVerbose("Stack Trace:\r{0}", e.StackTrace); return; } } diff --git a/src/PepperDash.Core/Logging/CrestronEnricher.cs b/src/PepperDash.Core/Logging/CrestronEnricher.cs index 1f879925..65369957 100644 --- a/src/PepperDash.Core/Logging/CrestronEnricher.cs +++ b/src/PepperDash.Core/Logging/CrestronEnricher.cs @@ -9,6 +9,9 @@ using System.Threading.Tasks; namespace PepperDash.Core.Logging; +/// +/// Enriches log events with Crestron-specific context properties, such as the application name based on the device platform. +/// public class CrestronEnricher : ILogEventEnricher { static readonly string _appName; @@ -27,6 +30,11 @@ public class CrestronEnricher : ILogEventEnricher } + /// + /// Enriches the log event with Crestron-specific properties. + /// + /// + /// public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var property = propertyFactory.CreateProperty("App", _appName); diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index ae353abe..2dfa7698 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reflection; using System.Text.RegularExpressions; +using System.Timers; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronDataStore; using Crestron.SimplSharp.CrestronIO; @@ -44,24 +45,32 @@ public static class Debug private static ILogger _logger; - private static readonly LoggingLevelSwitch _consoleLoggingLevelSwitch; + private static readonly LoggingLevelSwitch consoleLoggingLevelSwitch; - private static readonly LoggingLevelSwitch _websocketLoggingLevelSwitch; + private static readonly LoggingLevelSwitch websocketLoggingLevelSwitch; - private static readonly LoggingLevelSwitch _errorLogLevelSwitch; + private static readonly LoggingLevelSwitch errorLogLevelSwitch; - private static readonly LoggingLevelSwitch _fileLevelSwitch; + private static readonly LoggingLevelSwitch fileLevelSwitch; + /// + /// The minimum log level for messages to be sent to the console sink + /// public static LogEventLevel WebsocketMinimumLogLevel { - get { return _websocketLoggingLevelSwitch.MinimumLevel; } + get { return websocketLoggingLevelSwitch.MinimumLevel; } } - private static readonly DebugWebsocketSink _websocketSink; + private static readonly DebugWebsocketSink websocketSink; + /// + /// The DebugWebsocketSink instance used for sending log messages to connected websocket clients. + /// This is exposed publicly in case there is a need to call methods on the sink directly, such as SendMessageToClients. + /// For general logging purposes, use the LogMessage and LogError methods in this class which will send messages to all configured sinks including the websocket sink. + /// public static DebugWebsocketSink WebsocketSink { - get { return _websocketSink; } + get { return websocketSink; } } /// @@ -95,6 +104,9 @@ public static class Debug private const int SaveTimeoutMs = 30000; + /// + /// Indicates whether the code is running on an appliance or not. Used to determine file paths and other appliance vs server differences + /// public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; /// @@ -102,7 +114,12 @@ public static class Debug /// public static string PepperDashCoreVersion { get; private set; } - private static CTimer _saveTimer; + private static Timer _saveTimer; + + + private const int defaultConsoleDebugTimeoutMin = 120; + + private static Timer consoleDebugTimer; /// /// When true, the IncludedExcludedKeys dict will contain keys to include. @@ -118,6 +135,10 @@ public static class Debug private static LoggerConfiguration _loggerConfiguration; + /// + /// The default logger configuration used by the Debug class. Can be used as a base for creating custom logger configurations. + /// If changes are made to this configuration after initialization, call ResetLoggerConfiguration to have those changes reflected in the logger. + /// public static LoggerConfiguration LoggerConfiguration => _loggerConfiguration; static Debug() @@ -126,6 +147,13 @@ public static class Debug { CrestronDataStoreStatic.InitCrestronDataStore(); + consoleDebugTimer = new Timer(defaultConsoleDebugTimeoutMin * 60000) { AutoReset = false }; + consoleDebugTimer.Elapsed += (s, e) => + { + SetDebugLevel(LogEventLevel.Information); + CrestronConsole.ConsoleCommandResponse($"Console debug level reset to {LogEventLevel.Information} after timeout of {defaultConsoleDebugTimeoutMin} minutes"); + }; + var defaultConsoleLevel = GetStoredLogEventLevel(LevelStoreKey); var defaultWebsocketLevel = GetStoredLogEventLevel(WebSocketLevelStoreKey); @@ -134,15 +162,15 @@ public static class Debug var defaultFileLogLevel = GetStoredLogEventLevel(FileLevelStoreKey); - _consoleLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultConsoleLevel); + consoleLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultConsoleLevel); - _websocketLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultWebsocketLevel); + websocketLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultWebsocketLevel); - _errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel); + errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel); - _fileLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); + fileLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); - _websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); + websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); var logFilePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}app{InitialParametersClass.ApplicationNumber}{Path.DirectorySeparatorChar}global-log.log" : @@ -158,14 +186,14 @@ public static class Debug .MinimumLevel.Verbose() .Enrich.FromLogContext() .Enrich.With(new CrestronEnricher()) - .WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: _consoleLoggingLevelSwitch) - .WriteTo.Sink(_websocketSink, levelSwitch: _websocketLoggingLevelSwitch) - .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: _errorLogLevelSwitch) + .WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: consoleLoggingLevelSwitch) + .WriteTo.Sink(websocketSink, levelSwitch: websocketLoggingLevelSwitch) + .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: errorLogLevelSwitch) .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug, retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, - levelSwitch: _fileLevelSwitch + levelSwitch: fileLevelSwitch ); // Instantiate the root logger @@ -214,9 +242,9 @@ public static class Debug if (DoNotLoadConfigOnNextBoot) CrestronConsole.PrintLine(string.Format("Program {0} will not load config after next boot. Use console command go:{0} to load the config manually", InitialParametersClass.ApplicationNumber)); - _consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) => + consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) => { - LogMessage(LogEventLevel.Information, "Console debug level set to {minimumLevel}", _consoleLoggingLevelSwitch.MinimumLevel); + LogMessage(LogEventLevel.Information, "Console debug level set to {minimumLevel}", consoleLoggingLevelSwitch.MinimumLevel); }; } catch (Exception ex) @@ -238,6 +266,12 @@ public static class Debug return doNotLoad; } + /// + /// Updates the LoggerConfiguration used by the Debug class. + /// This allows for changing logger settings such as sinks and output templates. + /// After calling this method, the new configuration will be used for all subsequent log messages. + /// + /// public static void UpdateLoggerConfiguration(LoggerConfiguration config) { _loggerConfiguration = config; @@ -245,6 +279,9 @@ public static class Debug _logger = config.CreateLogger(); } + /// + /// Resets the LoggerConfiguration to the default configuration defined in this class. + /// public static void ResetLoggerConfiguration() { _loggerConfiguration = _defaultLoggerConfiguration; @@ -332,7 +369,11 @@ public static class Debug if (levelString.Trim() == "?") { CrestronConsole.ConsoleCommandResponse( + "Used to set the minimum level of debug messages to be printed to the console:\r\n" + + "[LogLevel] [TimeoutInMinutes]\r\n" + + "If TimeoutInMinutes is not provided, it will default to 120 minutes. If provided, the level will reset to Information after the timeout period elapses.\r\n" + + "LogLevel can be either a number from 0-5 or a log level name. If using a number, the mapping is as follows:\r\n" + $"{_logLevels[0]} = 0\r\n" + $"{_logLevels[1]} = 1\r\n" + $"{_logLevels[2]} = 2\r\n" + @@ -344,10 +385,22 @@ public static class Debug if (string.IsNullOrEmpty(levelString.Trim())) { - CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", _consoleLoggingLevelSwitch.MinimumLevel); + CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", consoleLoggingLevelSwitch.MinimumLevel); return; } + // split on space to allow for potential future addition of timeout parameter without breaking existing command usage + var parts = Regex.Split(levelString.Trim(), @"\s+"); + levelString = parts[0]; + + if (parts.Length > 1 && long.TryParse(parts[1], out var timeout)) + { + timeout = Math.Max(timeout, 1); // enforce minimum timeout of 1 minute + consoleDebugTimer.Interval = timeout * 60000; + } + + // first try to parse as int for backward compatibility with existing usage of numeric levels + if (int.TryParse(levelString, out var levelInt)) { if (levelInt < 0 || levelInt > 5) @@ -377,7 +430,8 @@ public static class Debug /// Sets the debug level /// /// Valid values 0-5 - public static void SetDebugLevel(uint level) + /// Timeout in minutes + public static void SetDebugLevel(uint level, int timeout = defaultConsoleDebugTimeoutMin) { if (!_logLevels.TryGetValue(level, out var logLevel)) { @@ -385,18 +439,27 @@ public static class Debug CrestronConsole.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}"); - SetDebugLevel(logLevel); + SetDebugLevel(logLevel, timeout); } - SetDebugLevel(logLevel); + SetDebugLevel(logLevel, timeout); } - public static void SetDebugLevel(LogEventLevel level) + /// + /// Sets the debug level + /// + /// The log level to set + /// Timeout in minutes + public static void SetDebugLevel(LogEventLevel level, int timeout = defaultConsoleDebugTimeoutMin) { - _consoleLoggingLevelSwitch.MinimumLevel = level; + consoleDebugTimer.Stop(); + consoleDebugTimer.Interval = timeout * 60000; + consoleDebugTimer.Start(); + + consoleLoggingLevelSwitch.MinimumLevel = level; CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}\r\n", - InitialParametersClass.ApplicationNumber, _consoleLoggingLevelSwitch.MinimumLevel); + InitialParametersClass.ApplicationNumber, consoleLoggingLevelSwitch.MinimumLevel); CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); @@ -408,40 +471,52 @@ public static class Debug CrestronConsole.PrintLine($"Error saving console debug level setting: {err}"); } + /// + /// Sets the debug level for the websocket sink + /// + /// public static void SetWebSocketMinimumDebugLevel(LogEventLevel level) { - _websocketLoggingLevelSwitch.MinimumLevel = level; + websocketLoggingLevelSwitch.MinimumLevel = level; var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint)level); if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {erro}", err); - LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); + LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", websocketLoggingLevelSwitch.MinimumLevel); } + + /// + /// Sets the minimum debug level for the error log sink + /// + /// public static void SetErrorLogMinimumDebugLevel(LogEventLevel level) { - _errorLogLevelSwitch.MinimumLevel = level; + errorLogLevelSwitch.MinimumLevel = level; var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting: {error}", err); - LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); + LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", websocketLoggingLevelSwitch.MinimumLevel); } + /// + /// Sets the minimum debug level for the file sink + /// public static void SetFileMinimumDebugLevel(LogEventLevel level) { - _errorLogLevelSwitch.MinimumLevel = level; + errorLogLevelSwitch.MinimumLevel = level; var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) LogMessage(LogEventLevel.Information, "Error saving File debug level setting: {error}", err); - LogMessage(LogEventLevel.Information, "File debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel); + LogMessage(LogEventLevel.Information, "File debug level set to {0}", websocketLoggingLevelSwitch.MinimumLevel); } /// @@ -626,21 +701,45 @@ public static class Debug } } + /// + /// Log a message with at the specified level. + /// public static void LogMessage(LogEventLevel level, string message, params object[] args) { _logger.Write(level, message, args); } + /// + /// Log a message with at the specified level and exception. + /// + /// + /// + /// + /// public static void LogMessage(LogEventLevel level, Exception ex, string message, params object[] args) { _logger.Write(level, ex, message, args); } + /// + /// Log a message with at the specified level and device context. + /// + /// + /// + /// public static void LogMessage(LogEventLevel level, IKeyed keyed, string message, params object[] args) { LogMessage(level, message, keyed, args); } + /// + /// Log a message with at the specified level, exception, and device context. + /// + /// + /// + /// + /// + /// public static void LogMessage(LogEventLevel level, Exception ex, IKeyed device, string message, params object[] args) { using (LogContext.PushProperty("Key", device?.Key)) @@ -650,6 +749,13 @@ public static class Debug } #region Explicit methods for logging levels + + /// + /// Log a message with Verbose level and device context. + /// + /// + /// + /// public static void LogVerbose(IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -658,6 +764,9 @@ public static class Debug } } + /// + /// Log a message with Verbose level, exception, and device context. + /// public static void LogVerbose(Exception ex, IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -666,16 +775,25 @@ public static class Debug } } + /// + /// Log a message with Verbose level. + /// public static void LogVerbose(string message, params object[] args) { _logger.Write(LogEventLevel.Verbose, message, args); } + /// + /// Log a message with Verbose level and exception. + /// public static void LogVerbose(Exception ex, string message, params object[] args) { _logger.Write(LogEventLevel.Verbose, ex, null, message, args); } + /// + /// Log a message with Debug level and device context. + /// public static void LogDebug(IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -684,6 +802,9 @@ public static class Debug } } + /// + /// Log a message with Debug level, exception, and device context. + /// public static void LogDebug(Exception ex, IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -692,16 +813,25 @@ public static class Debug } } + /// + /// Log a message with Debug level. + /// public static void LogDebug(string message, params object[] args) { _logger.Write(LogEventLevel.Debug, message, args); } + /// + /// Log a message with Debug level and exception. + /// public static void LogDebug(Exception ex, string message, params object[] args) { _logger.Write(LogEventLevel.Debug, ex, null, message, args); } + /// + /// Log a message with Information level and device context. + /// public static void LogInformation(IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -710,6 +840,9 @@ public static class Debug } } + /// + /// Log a message with Information level, exception, and device context. + /// public static void LogInformation(Exception ex, IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -718,16 +851,25 @@ public static class Debug } } + /// + /// Log a message with Information level. + /// public static void LogInformation(string message, params object[] args) { _logger.Write(LogEventLevel.Information, message, args); } + /// + /// Log a message with Information level and exception. + /// public static void LogInformation(Exception ex, string message, params object[] args) { _logger.Write(LogEventLevel.Information, ex, null, message, args); } + /// + /// Log a message with Warning level and device context. + /// public static void LogWarning(IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -736,6 +878,9 @@ public static class Debug } } + /// + /// Log a message with Warning level, exception, and device context. + /// public static void LogWarning(Exception ex, IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -744,16 +889,25 @@ public static class Debug } } + /// + /// Log a message with Warning level. + /// public static void LogWarning(string message, params object[] args) { _logger.Write(LogEventLevel.Warning, message, args); } + /// + /// Log a message with Warning level and exception. + /// public static void LogWarning(Exception ex, string message, params object[] args) { _logger.Write(LogEventLevel.Warning, ex, null, message, args); } + /// + /// Log a message with Error level and device context. + /// public static void LogError(IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -762,6 +916,9 @@ public static class Debug } } + /// + /// Log a message with Error level, exception, and device context. + /// public static void LogError(Exception ex, IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -770,16 +927,25 @@ public static class Debug } } + /// + /// Log a message with Error level. + /// public static void LogError(string message, params object[] args) { _logger.Write(LogEventLevel.Error, message, args); } + /// + /// Log a message with Error level and exception. + /// public static void LogError(Exception ex, string message, params object[] args) { _logger.Write(LogEventLevel.Error, ex, null, message, args); } + /// + /// Log a message with Fatal level and device context. + /// public static void LogFatal(IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -788,6 +954,9 @@ public static class Debug } } + /// + /// Log a message with Fatal level, exception, and device context. + /// public static void LogFatal(Exception ex, IKeyed keyed, string message, params object[] args) { using (LogContext.PushProperty("Key", keyed?.Key)) @@ -796,11 +965,19 @@ public static class Debug } } + /// + /// Log a message with Fatal level. + /// + /// + /// public static void LogFatal(string message, params object[] args) { _logger.Write(LogEventLevel.Fatal, message, args); } + /// + /// Log a message with Fatal level and exception. + /// public static void LogFatal(Exception ex, string message, params object[] args) { _logger.Write(LogEventLevel.Fatal, ex, null, message, args); @@ -828,127 +1005,28 @@ public static class Debug } - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. - /// Uses CrestronConsole.PrintLine. - /// - /// - /// Console format string - /// Object parameters - [Obsolete("Use LogMessage methods. Will be removed in 2.2.0 and later versions")] - public static void Console(uint level, string format, params object[] items) - { - - LogMessage(level, format, items); - - //if (IsRunningOnAppliance) - //{ - // CrestronConsole.PrintLine("[{0}]App {1} Lvl {2}:{3}", DateTime.Now.ToString("HH:mm:ss.fff"), - // InitialParametersClass.ApplicationNumber, - // level, - // string.Format(format, items)); - //} - } - - /// - /// Logs to Console when at-level, and all messages to error log, including device key - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void Console(uint level, IKeyed dev, string format, params object[] items) - { - LogMessage(level, dev, format, items); - - //if (Level >= level) - // Console(level, "[{0}] {1}", dev.Key, message); - } - - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. Always sends message to Error Log. - /// Uses CrestronConsole.PrintLine. - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - LogMessage(level, dev, format, items); - } - - /// - /// 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")] - public static void Console(uint level, ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - LogMessage(level, format, items); - } - - /// - /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at - /// or above the level provided, then the output will be written to both console and the log. Otherwise - /// it will only be written to the log. - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void ConsoleWithLog(uint level, string format, params object[] items) - { - LogMessage(level, format, items); - - // var str = string.Format(format, items); - //if (Level >= level) - // CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); - // CrestronLogger.WriteToLog(str, level); - } - - /// - /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at - /// or above the level provided, then the output will be written to both console and the log. Otherwise - /// it will only be written to the log. - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items) - { - LogMessage(level, dev, format, items); - - // var str = string.Format(format, items); - // CrestronLogger.WriteToLog(string.Format("[{0}] {1}", dev.Key, str), level); - } - - /// - /// Prints to log and error log - /// - /// - /// - [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - public static void LogError(ErrorLogLevel errorLogLevel, string str) - { - switch (errorLogLevel) - { - case ErrorLogLevel.Error: - LogMessage(LogEventLevel.Error, str); - break; - case ErrorLogLevel.Warning: - LogMessage(LogEventLevel.Warning, str); - break; - case ErrorLogLevel.Notice: - LogMessage(LogEventLevel.Information, str); - break; - } - } - /// /// Writes the memory object after timeout /// static void SaveMemoryOnTimeout() { - Console(0, "Saving debug settings"); + LogInformation("Saving debug settings"); if (_saveTimer == null) - _saveTimer = new CTimer(o => + { + _saveTimer = new Timer(SaveTimeoutMs) { AutoReset = false }; + _saveTimer.Elapsed += (s, e) => { _saveTimer = null; SaveMemory(); - }, SaveTimeoutMs); + }; + _saveTimer.Start(); + } else - _saveTimer.Reset(SaveTimeoutMs); + { + _saveTimer.Stop(); + _saveTimer.Interval = SaveTimeoutMs; + _saveTimer.Start(); + } } /// @@ -964,7 +1042,7 @@ public static class Debug { var fileName = GetMemoryFileName(); - LogMessage(LogEventLevel.Information, "Loading debug settings file from {fileName}", fileName); + LogInformation("Loading debug settings file from {fileName}", fileName); using (var sw = new StreamWriter(fileName)) { @@ -1018,27 +1096,4 @@ public static class Debug return string.Format("{0}{1}user{1}debugSettings{1}{2}.json", Directory.GetApplicationRootDirectory(), Path.DirectorySeparatorChar, InitialParametersClass.RoomId); } - - /// - /// Error level to for message to be logged at - /// - public enum ErrorLogLevel - { - /// - /// Error - /// - Error, - /// - /// Warning - /// - Warning, - /// - /// Notice - /// - Notice, - /// - /// None - /// - None, - } } \ No newline at end of file diff --git a/src/PepperDash.Core/Logging/DebugContext.cs b/src/PepperDash.Core/Logging/DebugContext.cs deleted file mode 100644 index 117140ab..00000000 --- a/src/PepperDash.Core/Logging/DebugContext.cs +++ /dev/null @@ -1,283 +0,0 @@ -extern alias NewtonsoftJson; - -using System; -using System.Collections.Generic; -using System.Linq; -using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronIO; -using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; -using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; - - -namespace PepperDash.Core; - -/// -/// Represents a debugging context -/// -public class DebugContext -{ - /// - /// Describes the folder location where a given program stores it's debug level memory. By default, the - /// file written will be named appNdebug where N is 1-10. - /// - public string Key { get; private set; } - - ///// - ///// The name of the file containing the current debug settings. - ///// - //string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber); - - DebugContextSaveData SaveData; - - int SaveTimeoutMs = 30000; - - CTimer SaveTimer; - - - static List Contexts = new List(); - - /// - /// Creates or gets a debug context - /// - /// - /// - public static DebugContext GetDebugContext(string key) - { - var context = Contexts.FirstOrDefault(c => c.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (context == null) - { - context = new DebugContext(key); - Contexts.Add(context); - } - return context; - } - - /// - /// Do not use. For S+ access. - /// - public DebugContext() { } - - DebugContext(string key) - { - Key = key; - if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) - { - // Add command to console - CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", - "appdebug:P [0-2]: Sets the application's console debug message level", - ConsoleAccessLevelEnum.AccessOperator); - } - - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - - LoadMemory(); - } - - /// - /// Used to save memory when shutting down - /// - /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping) - { - if (SaveTimer != null) - { - SaveTimer.Stop(); - SaveTimer = null; - } - Console(0, "Saving debug settings"); - SaveMemory(); - } - } - - /// - /// Callback for console command - /// - /// - public void SetDebugFromConsole(string levelString) - { - try - { - if (string.IsNullOrEmpty(levelString.Trim())) - { - CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", SaveData.Level); - return; - } - - SetDebugLevel(Convert.ToInt32(levelString)); - } - catch - { - CrestronConsole.PrintLine("Usage: appdebug:P [0-2]"); - } - } - - /// - /// Sets the debug level - /// - /// Valid values 0 (no debug), 1 (critical), 2 (all messages) - public void SetDebugLevel(int level) - { - if (level <= 2) - { - SaveData.Level = level; - SaveMemoryOnTimeout(); - - CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}", - InitialParametersClass.ApplicationNumber, SaveData.Level); - } - } - - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. - /// Uses CrestronConsole.PrintLine. - /// - /// - /// Console format string - /// Object parameters - public void Console(uint level, string format, params object[] items) - { - if (SaveData.Level >= level) - CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, - string.Format(format, items)); - } - - /// - /// Appends a device Key to the beginning of a message - /// - public void Console(uint level, IKeyed dev, string format, params object[] items) - { - if (SaveData.Level >= level) - Console(level, "[{0}] {1}", dev.Key, string.Format(format, items)); - } - - /// - /// - /// - /// - /// - /// - /// - /// - public void Console(uint level, IKeyed dev, Debug.ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - if (SaveData.Level >= level) - { - var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items)); - Console(level, str); - LogError(errorLogLevel, str); - } - } - - /// - /// - /// - /// - /// - /// - /// - public void Console(uint level, Debug.ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - if (SaveData.Level >= level) - { - var str = string.Format(format, items); - Console(level, str); - LogError(errorLogLevel, str); - } - } - - /// - /// - /// - /// - /// - public void LogError(Debug.ErrorLogLevel errorLogLevel, string str) - { - string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); - switch (errorLogLevel) - { - case Debug.ErrorLogLevel.Error: - ErrorLog.Error(msg); - break; - case Debug.ErrorLogLevel.Warning: - ErrorLog.Warn(msg); - break; - case Debug.ErrorLogLevel.Notice: - ErrorLog.Notice(msg); - break; - } - } - - /// - /// Writes the memory object after timeout - /// - void SaveMemoryOnTimeout() - { - if (SaveTimer == null) - SaveTimer = new CTimer(o => - { - SaveTimer = null; - SaveMemory(); - }, SaveTimeoutMs); - else - SaveTimer.Reset(SaveTimeoutMs); - } - - /// - /// Writes the memory - use SaveMemoryOnTimeout - /// - void SaveMemory() - { - using (StreamWriter sw = new StreamWriter(GetMemoryFileName())) - { - var json = JsonConvert.SerializeObject(SaveData); - sw.Write(json); - sw.Flush(); - } - } - - /// - /// - /// - void LoadMemory() - { - var file = GetMemoryFileName(); - if (File.Exists(file)) - { - using (StreamReader sr = new StreamReader(file)) - { - var data = JsonConvert.DeserializeObject(sr.ReadToEnd()); - if (data != null) - { - SaveData = data; - Debug.Console(1, "Debug memory restored from file"); - return; - } - else - SaveData = new DebugContextSaveData(); - } - } - } - - /// - /// Helper to get the file path for this app's debug memory - /// - string GetMemoryFileName() - { - return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key); - } -} - -/// -/// -/// -public class DebugContextSaveData -{ - /// - /// - /// - public int Level { get; set; } -} \ No newline at end of file diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index f6e5a2f4..b9c74e68 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -105,7 +105,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0); - CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress)); + Debug.LogInformation("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress); var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), [string.Format("{0}.{1}", hostName, domainName), ipAddress], [KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth]); @@ -118,8 +118,8 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed } catch (Exception ex) { - //Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); - CrestronConsole.PrintLine("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); + Debug.LogError(ex, "WSS CreateCert Failed: {0}", ex.Message); + Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace); } } @@ -149,7 +149,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed /// The port number on which the WebSocket server will listen. Must be a valid, non-negative port number. public void StartServerAndSetPort(int port) { - Debug.Console(0, "Starting Websocket Server on port: {0}", port); + Debug.LogInformation("Starting Websocket Server on port: {0}", port); Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword); @@ -163,7 +163,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed if (!string.IsNullOrWhiteSpace(certPath)) { - Debug.Console(0, "Assigning SSL Configuration"); + Debug.LogInformation("Assigning SSL Configuration"); _httpsServer.SslConfiguration.ServerCertificate = new X509Certificate2(certPath, certPassword); _httpsServer.SslConfiguration.ClientCertificateRequired = false; @@ -172,13 +172,13 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed //this is just to test, you might want to actually validate _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { - Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered"); + Debug.LogInformation("HTTPS ClientCerticateValidation Callback triggered"); return true; }; } - Debug.Console(0, "Adding Debug Client Service"); + Debug.LogInformation("Adding Debug Client Service"); _httpsServer.AddWebSocketService(_path); - Debug.Console(0, "Assigning Log Info"); + Debug.LogInformation("Assigning Log Info"); _httpsServer.Log.Level = LogLevel.Trace; _httpsServer.Log.Output = (d, s) => { @@ -208,17 +208,17 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed level = 4; break; } - - Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); + Debug.LogInformation("{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); }; - Debug.Console(0, "Starting"); + Debug.LogInformation("Starting"); _httpsServer.Start(); - Debug.Console(0, "Ready"); + Debug.LogInformation("Ready"); } catch (Exception ex) { - Debug.Console(0, "WebSocket Failed to start {0}", ex.Message); + Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message); + Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace); } } @@ -229,7 +229,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed /// calling this method, the server will no longer accept or process incoming connections. public void StopServer() { - Debug.Console(0, "Stopping Websocket Server"); + Debug.LogInformation("Stopping Websocket Server"); _httpsServer?.Stop(); _httpsServer = null; @@ -291,20 +291,18 @@ public class DebugClient : WebSocketBehavior /// /// Initializes a new instance of the class. /// - /// This constructor creates a new instance and logs its - /// creation using the method with a debug level of 0. public DebugClient() { - Debug.Console(0, "DebugClient Created"); + Debug.LogInformation("DebugClient Created"); } - + /// protected override void OnOpen() { base.OnOpen(); var url = Context.WebSocket.Url; - Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); + Debug.LogInformation("New WebSocket Connection from: {0}", url); _connectionTime = DateTime.Now; } @@ -314,7 +312,7 @@ public class DebugClient : WebSocketBehavior { base.OnMessage(e); - Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data); + Debug.LogVerbose("WebSocket UiClient Message: {0}", e.Data); } /// @@ -322,8 +320,7 @@ public class DebugClient : WebSocketBehavior { base.OnClose(e); - Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); - + Debug.LogDebug("WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); } /// @@ -331,6 +328,7 @@ public class DebugClient : WebSocketBehavior { base.OnError(e); - Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); + Debug.LogError(e.Exception, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); + Debug.LogVerbose("Stack Trace:\r{0}", e.Exception.StackTrace); } } diff --git a/src/PepperDash.Core/PasswordManagement/PasswordManager.cs b/src/PepperDash.Core/PasswordManagement/PasswordManager.cs index d08a1010..a1c869a8 100644 --- a/src/PepperDash.Core/PasswordManagement/PasswordManager.cs +++ b/src/PepperDash.Core/PasswordManagement/PasswordManager.cs @@ -1,246 +1,252 @@ using System; using System.Collections.Generic; -using Crestron.SimplSharp; +using System.Timers; namespace PepperDash.Core.PasswordManagement; /// /// Allows passwords to be stored and managed /// - public class PasswordManager +public class PasswordManager +{ + /// + /// Public dictionary of known passwords + /// + public static Dictionary Passwords = new Dictionary(); + /// + /// Private dictionary, used when passwords are updated + /// + private Dictionary _passwords = new Dictionary(); + + /// + /// Timer used to wait until password changes have stopped before updating the dictionary + /// + Timer PasswordTimer; + /// + /// Timer length + /// + public long PasswordTimerElapsedMs = 5000; + + /// + /// Boolean event + /// + public event EventHandler BoolChange; + /// + /// Ushort event + /// + public event EventHandler UshrtChange; + /// + /// String event + /// + public event EventHandler StringChange; + /// + /// Event to notify clients of an updated password at the specified index (uint) + /// + public static event EventHandler PasswordChange; + + /// + /// Constructor + /// + public PasswordManager() { - /// - /// Public dictionary of known passwords - /// - public static Dictionary Passwords = new Dictionary(); - /// - /// Private dictionary, used when passwords are updated - /// - private Dictionary _passwords = new Dictionary(); - /// - /// Timer used to wait until password changes have stopped before updating the dictionary - /// - CTimer PasswordTimer; - /// - /// Timer length - /// - public long PasswordTimerElapsedMs = 5000; + } - /// - /// Boolean event - /// - public event EventHandler BoolChange; - /// - /// Ushort event - /// - public event EventHandler UshrtChange; - /// - /// String event - /// - public event EventHandler StringChange; - /// - /// Event to notify clients of an updated password at the specified index (uint) - /// - public static event EventHandler PasswordChange; + /// + /// Initialize password manager + /// + public void Initialize() + { + if (Passwords == null) + Passwords = new Dictionary(); - /// - /// Constructor - /// - public PasswordManager() + if (_passwords == null) + _passwords = new Dictionary(); + + OnBoolChange(true, 0, PasswordManagementConstants.PasswordInitializedChange); + } + + /// + /// Updates password stored in the dictonary + /// + /// + /// + /// + /// UpdatePassword method + /// + public void UpdatePassword(ushort key, string password) + { + // validate the parameters + if (key > 0 && string.IsNullOrEmpty(password)) { - + Debug.LogDebug("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password); + return; } - /// - /// Initialize password manager - /// - public void Initialize() + try { - if (Passwords == null) - Passwords = new Dictionary(); + // if key exists, update the value + if (_passwords.ContainsKey(key)) + _passwords[key] = password; + // else add the key & value + else + _passwords.Add(key, password); - if (_passwords == null) - _passwords = new Dictionary(); + Debug.LogDebug("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key]); - OnBoolChange(true, 0, PasswordManagementConstants.PasswordInitializedChange); - } - - /// - /// Updates password stored in the dictonary - /// - /// - /// - /// - /// UpdatePassword method - /// - public void UpdatePassword(ushort key, string password) - { - // validate the parameters - if (key > 0 && string.IsNullOrEmpty(password)) + if (PasswordTimer == null) { - Debug.Console(1, string.Format("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password)); - return; + PasswordTimer = new Timer(PasswordTimerElapsedMs) { AutoReset = false }; + PasswordTimer.Elapsed += (s, e) => PasswordTimerElapsed(s, e); + PasswordTimer.Start(); + Debug.LogDebug("PasswordManager.UpdatePassword: Timer Started"); + OnBoolChange(true, 0, PasswordManagementConstants.PasswordUpdateBusyChange); } - - try - { - // if key exists, update the value - if(_passwords.ContainsKey(key)) - _passwords[key] = password; - // else add the key & value - else - _passwords.Add(key, password); - - Debug.Console(1, string.Format("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key])); - - if (PasswordTimer == null) - { - PasswordTimer = new CTimer((o) => PasswordTimerElapsed(), PasswordTimerElapsedMs); - Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Started")); - OnBoolChange(true, 0, PasswordManagementConstants.PasswordUpdateBusyChange); - } - else - { - PasswordTimer.Reset(PasswordTimerElapsedMs); - Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Reset")); - } - } - catch (Exception e) - { - var msg = string.Format("PasswordManager.UpdatePassword key-value[{0}, {1}] failed:\r{2}", key, password, e); - Debug.Console(1, msg); - } - } - - /// - /// CTimer callback function - /// - private void PasswordTimerElapsed() - { - try + else { PasswordTimer.Stop(); - Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: CTimer Stopped")); - OnBoolChange(false, 0, PasswordManagementConstants.PasswordUpdateBusyChange); - foreach (var pw in _passwords) + PasswordTimer.Interval = PasswordTimerElapsedMs; + PasswordTimer.Start(); + Debug.LogDebug("PasswordManager.UpdatePassword: Timer Reset"); + } + } + catch (Exception e) + { + var msg = string.Format("PasswordManager.UpdatePassword key-value[{0}, {1}] failed:\r{2}", key, password, e); + Debug.LogError(e, msg); + Debug.LogVerbose("Stack Trace:\r{0}", e.StackTrace); + } + } + + /// + /// Timer callback function + /// + private void PasswordTimerElapsed(object sender, ElapsedEventArgs e) + { + try + { + PasswordTimer.Stop(); + Debug.LogDebug("PasswordManager.PasswordTimerElapsed: Timer Stopped"); + OnBoolChange(false, 0, PasswordManagementConstants.PasswordUpdateBusyChange); + foreach (var pw in _passwords) + { + // if key exists, continue + if (Passwords.ContainsKey(pw.Key)) { - // if key exists, continue - if (Passwords.ContainsKey(pw.Key)) + Debug.LogDebug("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value); + if (Passwords[pw.Key] != _passwords[pw.Key]) { - Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value)); - if (Passwords[pw.Key] != _passwords[pw.Key]) - { - Passwords[pw.Key] = _passwords[pw.Key]; - Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key])); - OnPasswordChange(Passwords[pw.Key], (ushort)pw.Key, PasswordManagementConstants.StringValueChange); - } - } - // else add the key & value - else - { - Passwords.Add(pw.Key, pw.Value); + Passwords[pw.Key] = _passwords[pw.Key]; + Debug.LogDebug("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key]); + OnPasswordChange(Passwords[pw.Key], (ushort)pw.Key, PasswordManagementConstants.StringValueChange); } } - OnUshrtChange((ushort)Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange); - } - catch (Exception e) - { - var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", e); - Debug.Console(1, msg); + // else add the key & value + else + { + Passwords.Add(pw.Key, pw.Value); + } } + OnUshrtChange((ushort)Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange); } - - /// - /// Method to change the default timer value, (default 5000ms/5s) - /// - /// - /// - /// PasswordTimerMs method - /// - public void PasswordTimerMs(ushort time) + catch (Exception ex) { - PasswordTimerElapsedMs = Convert.ToInt64(time); + var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", ex.Message); + Debug.LogError(ex, msg); + Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace); } + } - /// - /// Helper method for debugging to see what passwords are in the lists - /// - public void ListPasswords() - { - Debug.Console(0, "PasswordManager.ListPasswords:\r"); - foreach (var pw in Passwords) - Debug.Console(0, "Passwords[{0}]: {1}\r", pw.Key, pw.Value); - Debug.Console(0, "\n"); - foreach (var pw in _passwords) - Debug.Console(0, "_passwords[{0}]: {1}\r", pw.Key, pw.Value); - } + /// + /// Method to change the default timer value, (default 5000ms/5s) + /// + /// + /// + /// PasswordTimerMs method + /// + public void PasswordTimerMs(ushort time) + { + PasswordTimerElapsedMs = Convert.ToInt64(time); + } - /// - /// Protected boolean change event handler - /// - /// - /// - /// - protected void OnBoolChange(bool state, ushort index, ushort type) - { - var handler = BoolChange; - if (handler != null) - { - var args = new BoolChangeEventArgs(state, type); - args.Index = index; - BoolChange(this, args); - } - } + /// + /// Helper method for debugging to see what passwords are in the lists + /// + public void ListPasswords() + { + Debug.LogInformation("PasswordManager.ListPasswords:\r"); + foreach (var pw in Passwords) + Debug.LogInformation("Passwords[{0}]: {1}\r", pw.Key, pw.Value); + Debug.LogInformation("\n"); + foreach (var pw in _passwords) + Debug.LogInformation("_passwords[{0}]: {1}\r", pw.Key, pw.Value); + } - /// - /// Protected ushort change event handler - /// - /// - /// - /// - protected void OnUshrtChange(ushort value, ushort index, ushort type) + /// + /// Protected boolean change event handler + /// + /// + /// + /// + protected void OnBoolChange(bool state, ushort index, ushort type) + { + var handler = BoolChange; + if (handler != null) { - var handler = UshrtChange; - if (handler != null) - { - var args = new UshrtChangeEventArgs(value, type); - args.Index = index; - UshrtChange(this, args); - } + var args = new BoolChangeEventArgs(state, type); + args.Index = index; + BoolChange(this, args); } + } - /// - /// Protected string change event handler - /// - /// - /// - /// - protected void OnStringChange(string value, ushort index, ushort type) + /// + /// Protected ushort change event handler + /// + /// + /// + /// + protected void OnUshrtChange(ushort value, ushort index, ushort type) + { + var handler = UshrtChange; + if (handler != null) { - var handler = StringChange; - if (handler != null) - { - var args = new StringChangeEventArgs(value, type); - args.Index = index; - StringChange(this, args); - } + var args = new UshrtChangeEventArgs(value, type); + args.Index = index; + UshrtChange(this, args); } + } - /// - /// Protected password change event handler - /// - /// - /// - /// - protected void OnPasswordChange(string value, ushort index, ushort type) + /// + /// Protected string change event handler + /// + /// + /// + /// + protected void OnStringChange(string value, ushort index, ushort type) + { + var handler = StringChange; + if (handler != null) { - var handler = PasswordChange; - if (handler != null) - { - var args = new StringChangeEventArgs(value, type); - args.Index = index; - PasswordChange(this, args); - } + var args = new StringChangeEventArgs(value, type); + args.Index = index; + StringChange(this, args); } - } \ No newline at end of file + } + + /// + /// Protected password change event handler + /// + /// + /// + /// + protected void OnPasswordChange(string value, ushort index, ushort type) + { + var handler = PasswordChange; + if (handler != null) + { + var args = new StringChangeEventArgs(value, type); + args.Index = index; + PasswordChange(this, args); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/eSourceListItemDestinationTypes.cs b/src/PepperDash.Essentials.Core/Devices/eSourceListItemDestinationTypes.cs deleted file mode 100644 index 7b9e9b35..00000000 --- a/src/PepperDash.Essentials.Core/Devices/eSourceListItemDestinationTypes.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; - -namespace PepperDash.Essentials.Core; - -/// -/// Defines the eSourceListItemDestinationTypes enumeration, which represents the various destination types for source list items in a room control system. -/// This enumeration is marked as obsolete, indicating that it may be removed in future versions and should not be used in new development. -/// Each member of the enumeration corresponds to a specific type of display or audio output commonly found in room control systems, -/// such as default displays, program audio, codec content, and auxiliary displays. -/// -[Obsolete] -public enum eSourceListItemDestinationTypes -{ - /// - /// Default display, used for the main video output in a room - /// - defaultDisplay, - /// - /// Left display - /// - leftDisplay, - /// - /// Right display - /// - rightDisplay, - /// - /// Center display - /// - centerDisplay, - /// - /// Program audio, used for the main audio output in a room - /// - programAudio, - /// - /// Codec content, used for sharing content to the far end in a video call - /// - codecContent, - /// - /// Front left display, used for rooms with multiple displays - /// - frontLeftDisplay, - /// - /// Front right display, used for rooms with multiple displays - /// - frontRightDisplay, - /// - /// Rear left display, used for rooms with multiple displays - /// - rearLeftDisplay, - /// - /// Rear right display, used for rooms with multiple displays - /// - rearRightDisplay, - /// - /// Auxiliary display 1, used for additional displays in a room - /// - auxDisplay1, - /// - /// Auxiliary display 2, used for additional displays in a room - /// - auxDisplay2, - /// - /// Auxiliary display 3, used for additional displays in a room - /// - auxDisplay3, - /// - /// Auxiliary display 4, used for additional displays in a room - /// - auxDisplay4, - /// - /// Auxiliary display 5, used for additional displays in a room - /// - auxDisplay5, - /// - /// Auxiliary display 6, used for additional displays in a room - /// - auxDisplay6, - /// - /// Auxiliary display 7, used for additional displays in a room - /// - auxDisplay7, - /// - /// Auxiliary display 8, used for additional displays in a room - /// - auxDisplay8, - /// - /// Auxiliary display 9, used for additional displays in a room - /// - auxDisplay9, - /// - /// Auxiliary display 10, used for additional displays in a room - /// - auxDisplay10, -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackOneShot.cs b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackOneShot.cs index d0d0083e..74341438 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackOneShot.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackOneShot.cs @@ -1,4 +1,4 @@ -using Crestron.SimplSharp; +using System.Timers; namespace PepperDash.Essentials.Core { /// @@ -20,7 +20,7 @@ namespace PepperDash.Essentials.Core /// Gets or sets the Feedback /// public BoolFeedback Feedback { get; private set; } - CTimer Timer; + Timer Timer; bool _BoolValue; @@ -51,16 +51,22 @@ namespace PepperDash.Essentials.Core { _BoolValue = true; Feedback.FireUpdate(); - Timer = new CTimer(o => + Timer = new Timer(TimeoutMs) { AutoReset = false }; + Timer.Elapsed += (s, e) => { _BoolValue = false; Feedback.FireUpdate(); Timer = null; - }, TimeoutMs); + }; + Timer.Start(); } // Timer is running, if retrigger is set, reset it. else if (CanRetrigger) - Timer.Reset(TimeoutMs); + { + Timer.Stop(); + Timer.Interval = TimeoutMs; + Timer.Start(); + } } /// @@ -69,7 +75,11 @@ namespace PepperDash.Essentials.Core public void Cancel() { if (Timer != null) - Timer.Reset(0); + { + Timer.Stop(); + Timer.Interval = 1; + Timer.Start(); + } } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackPulseExtender.cs b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackPulseExtender.cs index 87c6e9d7..207c6fd6 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackPulseExtender.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedbackPulseExtender.cs @@ -2,8 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro; +using System.Timers; namespace PepperDash.Essentials.Core; /// @@ -21,7 +20,7 @@ namespace PepperDash.Essentials.Core; /// Gets the Feedback /// public BoolFeedback Feedback { get; private set; } - CTimer Timer; + Timer Timer; /// /// When set to true, will cause Feedback to go high, and cancel the timer. @@ -49,7 +48,11 @@ namespace PepperDash.Essentials.Core; else { if (Timer == null) - Timer = new CTimer(o => ClearFeedback(), TimeoutMs); + { + Timer = new Timer(TimeoutMs) { AutoReset = false }; + Timer.Elapsed += (s, e) => ClearFeedback(); + Timer.Start(); + } } } } diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index 0efdf5e1..2da18352 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Timers; using Timer = System.Timers.Timer; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; @@ -62,7 +63,7 @@ namespace PepperDash.Essentials.Core.Fusion private Event _currentMeeting; private RoomSchedule _currentSchedule; - private CTimer _dailyTimeRequestTimer; + private Timer _dailyTimeRequestTimer; private StatusMonitorCollection _errorMessageRollUp; private FusionRoomGuids _guids; @@ -70,9 +71,9 @@ namespace PepperDash.Essentials.Core.Fusion private bool _isRegisteredForSchedulePushNotifications; private Event _nextMeeting; - private CTimer _pollTimer; + private Timer _pollTimer; - private CTimer _pushNotificationTimer; + private Timer _pushNotificationTimer; private string _roomOccupancyRemoteString; @@ -729,15 +730,15 @@ namespace PepperDash.Essentials.Core.Fusion RequestLocalDateTime(null); // Setup timer to request time daily - if (_dailyTimeRequestTimer != null && !_dailyTimeRequestTimer.Disposed) + if (_dailyTimeRequestTimer != null) { _dailyTimeRequestTimer.Stop(); _dailyTimeRequestTimer.Dispose(); } - _dailyTimeRequestTimer = new CTimer(RequestLocalDateTime, null, 86400000, 86400000); - - _dailyTimeRequestTimer.Reset(86400000, 86400000); + _dailyTimeRequestTimer = new Timer(86400000) { AutoReset = true }; + _dailyTimeRequestTimer.Elapsed += (s, e) => RequestLocalDateTime(null); + _dailyTimeRequestTimer.Start(); }); } @@ -950,25 +951,25 @@ namespace PepperDash.Essentials.Core.Fusion { case 1: _isRegisteredForSchedulePushNotifications = true; - if (_pollTimer != null && !_pollTimer.Disposed) + if (_pollTimer != null) { _pollTimer.Stop(); _pollTimer.Dispose(); } - _pushNotificationTimer = new CTimer(RequestFullRoomSchedule, null, - PushNotificationTimeout, PushNotificationTimeout); - _pushNotificationTimer.Reset(PushNotificationTimeout, PushNotificationTimeout); + _pushNotificationTimer = new Timer(PushNotificationTimeout) { AutoReset = true }; + _pushNotificationTimer.Elapsed += (s, e) => RequestFullRoomSchedule(null); + _pushNotificationTimer.Start(); break; case 0: _isRegisteredForSchedulePushNotifications = false; - if (_pushNotificationTimer != null && !_pushNotificationTimer.Disposed) + if (_pushNotificationTimer != null) { _pushNotificationTimer.Stop(); _pushNotificationTimer.Dispose(); } - _pollTimer = new CTimer(RequestFullRoomSchedule, null, SchedulePollInterval, - SchedulePollInterval); - _pollTimer.Reset(SchedulePollInterval, SchedulePollInterval); + _pollTimer = new Timer(SchedulePollInterval) { AutoReset = true }; + _pollTimer.Elapsed += (s, e) => RequestFullRoomSchedule(null); + _pollTimer.Start(); break; } } @@ -1121,7 +1122,10 @@ namespace PepperDash.Essentials.Core.Fusion if (action.OuterXml.IndexOf("RequestSchedule", StringComparison.Ordinal) > -1) { - _pushNotificationTimer.Reset(PushNotificationTimeout, PushNotificationTimeout); + _pushNotificationTimer.Stop(); + _pushNotificationTimer.Interval = PushNotificationTimeout; + _pushNotificationTimer.Start(); + Debug.LogMessage(LogEventLevel.Verbose, this, "Received push notification for schedule change"); } } else // Not a push notification @@ -1177,7 +1181,9 @@ namespace PepperDash.Essentials.Core.Fusion if (!_isRegisteredForSchedulePushNotifications) { - _pollTimer.Reset(SchedulePollInterval, SchedulePollInterval); + _pollTimer.Stop(); + _pollTimer.Interval = SchedulePollInterval; + _pollTimer.Start(); } // Fire Schedule Change Event diff --git a/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs b/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs deleted file mode 100644 index 78446c6f..00000000 --- a/src/PepperDash.Essentials.Core/Interfaces/ILogStrings.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using PepperDash.Core; - -namespace PepperDash.Essentials.Core.Interfaces; - -/// -/// Defines the contract for ILogStrings -/// -public interface ILogStrings : IKeyed -{ - /// - /// Defines a class that is capable of logging a string - /// - void SendToLog(IKeyed device, string logMessage); -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs b/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs deleted file mode 100644 index b9d61df1..00000000 --- a/src/PepperDash.Essentials.Core/Interfaces/ILogStringsWithLevel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using PepperDash.Core; - -namespace PepperDash.Essentials.Core.Interfaces; - -/// -/// Defines the contract for ILogStringsWithLevel -/// -public interface ILogStringsWithLevel : IKeyed -{ - /// - /// Defines a class that is capable of logging a string with an int level - /// - void SendToLog(IKeyed device, Debug.ErrorLogLevel level, string logMessage); -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs b/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs index 67ba1047..381ac6ba 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs @@ -7,6 +7,7 @@ using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; using System.ComponentModel; +using System.Timers; using PepperDash.Core; @@ -84,10 +85,8 @@ namespace PepperDash.Essentials.Core long WarningTime; long ErrorTime; - CTimer WarningTimer; - CTimer ErrorTimer; - - /// + Timer WarningTimer; + Timer ErrorTimer; /// Constructor /// /// parent device @@ -155,8 +154,18 @@ namespace PepperDash.Essentials.Core /// protected void StartErrorTimers() { - if (WarningTimer == null) WarningTimer = new CTimer(o => { Status = MonitorStatus.InWarning; }, WarningTime); - if (ErrorTimer == null) ErrorTimer = new CTimer(o => { Status = MonitorStatus.InError; }, ErrorTime); + if (WarningTimer == null) + { + WarningTimer = new Timer(WarningTime) { AutoReset = false }; + WarningTimer.Elapsed += (s, e) => { Status = MonitorStatus.InWarning; }; + WarningTimer.Start(); + } + if (ErrorTimer == null) + { + ErrorTimer = new Timer(ErrorTime) { AutoReset = false }; + ErrorTimer.Elapsed += (s, e) => { Status = MonitorStatus.InError; }; + ErrorTimer.Start(); + } } /// @@ -176,10 +185,17 @@ namespace PepperDash.Essentials.Core protected void ResetErrorTimers() { if (WarningTimer != null) - WarningTimer.Reset(WarningTime, WarningTime); - if (ErrorTimer != null) - ErrorTimer.Reset(ErrorTime, ErrorTime); - + { + WarningTimer.Stop(); + WarningTimer.Interval = WarningTime; + WarningTimer.Start(); + } + if (ErrorTimer != null) + { + ErrorTimer.Stop(); + ErrorTimer.Interval = ErrorTime; + ErrorTimer.Start(); + } } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs b/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs index 875dfd9b..e4784250 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters; using PepperDash.Essentials.Core.Bridges; using Serilog.Events; using System.Threading.Tasks; +using System.Timers; namespace PepperDash.Essentials.Core.Monitoring; @@ -20,7 +21,7 @@ namespace PepperDash.Essentials.Core.Monitoring; public class SystemMonitorController : EssentialsBridgeableDevice { private const long UptimePollTime = 300000; - private CTimer _uptimePollTimer; + private Timer _uptimePollTimer; private string _uptime; private string _lastStart; @@ -147,7 +148,10 @@ public class SystemMonitorController : EssentialsBridgeableDevice CreateEthernetStatusFeedbacks(); UpdateEthernetStatusFeeedbacks(); - _uptimePollTimer = new CTimer(PollUptime, null, 0, UptimePollTime); + _uptimePollTimer = new Timer(UptimePollTime) { AutoReset = true }; + _uptimePollTimer.Elapsed += (s, e) => PollUptime(null); + _uptimePollTimer.Start(); + PollUptime(null); SystemMonitor.ProgramChange += SystemMonitor_ProgramChange; SystemMonitor.TimeZoneInformation.TimeZoneChange += TimeZoneInformation_TimeZoneChange; diff --git a/src/PepperDash.Essentials.Core/Ramps and Increments/ActionIncrementer.cs b/src/PepperDash.Essentials.Core/Ramps and Increments/ActionIncrementer.cs index ded7d9f3..952b2acf 100644 --- a/src/PepperDash.Essentials.Core/Ramps and Increments/ActionIncrementer.cs +++ b/src/PepperDash.Essentials.Core/Ramps and Increments/ActionIncrementer.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Crestron.SimplSharp; +using System.Timers; using PepperDash.Core; @@ -15,15 +15,30 @@ namespace PepperDash.Essentials.Core; /// public class ActionIncrementer { + /// + /// The amount to change the value by each increment + /// public int ChangeAmount { get; set; } + /// + /// The maximum value the incrementer can reach + /// public int MaxValue { get; set; } + /// + /// The minimum value the incrementer can reach + /// public int MinValue { get; set; } + /// + /// The delay before the incrementer starts repeating + /// public uint RepeatDelay { get; set; } + /// + /// The time interval between each repeat + /// public uint RepeatTime { get; set; } Action SetAction; Func GetFunc; - CTimer Timer; + Timer Timer; /// /// @@ -89,7 +104,20 @@ public class ActionIncrementer if (atLimit) // Don't go past end Stop(); else if (Timer == null) // Only enter the timer if it's not already running - Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime); + { + Timer = new Timer(RepeatDelay) { AutoReset = false }; + Timer.Elapsed += (s, e) => + { + Go(change); + if (Timer != null) + { + Timer.Interval = RepeatTime; + Timer.AutoReset = true; + Timer.Start(); + } + }; + Timer.Start(); + } } /// diff --git a/src/PepperDash.Essentials.Core/Ramps and Increments/UshortSigIncrementer.cs b/src/PepperDash.Essentials.Core/Ramps and Increments/UshortSigIncrementer.cs index 7ee4c956..eae6c21f 100644 --- a/src/PepperDash.Essentials.Core/Ramps and Increments/UshortSigIncrementer.cs +++ b/src/PepperDash.Essentials.Core/Ramps and Increments/UshortSigIncrementer.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Crestron.SimplSharp; +using System.Timers; using Crestron.SimplSharpPro; using PepperDash.Core; @@ -16,14 +16,39 @@ namespace PepperDash.Essentials.Core; public class UshortSigIncrementer { UShortInputSig TheSig; + + /// + /// Amount to change the signal on each step + /// public ushort ChangeAmount { get; set; } + /// + /// Maximum value to ramp to + /// public int MaxValue { get; set; } + /// + /// Minimum value to ramp to + /// public int MinValue { get; set; } + /// + /// The delay before the incrementer starts repeating + /// public uint RepeatDelay { get; set; } + /// + /// The time interval between each repeat + /// public uint RepeatTime { get; set; } bool SignedMode; - CTimer Timer; + Timer Timer; + /// + /// Constructor + /// + /// + /// + /// + /// + /// + /// public UshortSigIncrementer(UShortInputSig sig, ushort changeAmount, int minValue, int maxValue, uint repeatDelay, uint repeatTime) { TheSig = sig; @@ -37,12 +62,19 @@ public class UshortSigIncrementer Debug.LogMessage(LogEventLevel.Debug, "UshortSigIncrementer has signed values that exceed range of -32768, 32767"); } + /// + /// Starts incrementing cycle + /// + public void StartUp() { if (Timer != null) return; Go(ChangeAmount); } + /// + /// Starts decrementing cycle + /// public void StartDown() { if (Timer != null) return; @@ -64,7 +96,20 @@ public class UshortSigIncrementer if (atLimit) // Don't go past end Stop(); else if (Timer == null) // Only enter the timer if it's not already running - Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime); + { + Timer = new Timer(RepeatDelay) { AutoReset = false }; + Timer.Elapsed += (s, e) => + { + Go(change); + if (Timer != null) + { + Timer.Interval = RepeatTime; + Timer.AutoReset = true; + Timer.Start(); + } + }; + Timer.Start(); + } } bool CheckLevel(int levelIn, out int levelOut) @@ -85,6 +130,9 @@ public class UshortSigIncrementer return IsAtLimit; } + /// + /// Stops incrementing/decrementing cycle + /// public void Stop() { if (Timer != null) @@ -92,6 +140,11 @@ public class UshortSigIncrementer Timer = null; } + /// + /// Sets the value of the signal + /// + /// + void SetValue(ushort value) { //CrestronConsole.PrintLine("Increment level:{0} / {1}", value, (short)value); diff --git a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs index 3a9a7409..64a468db 100644 --- a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs +++ b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs @@ -1,12 +1,13 @@ -using Crestron.SimplSharp; -using PepperDash.Core; +using PepperDash.Core; using PepperDash.Core.Logging; using Serilog.Events; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Timer = System.Timers.Timer; namespace PepperDash.Essentials.Core; @@ -73,7 +74,7 @@ public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner } } - private CTimer _scenarioChangeDebounceTimer; + private Timer _scenarioChangeDebounceTimer; private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s @@ -204,18 +205,22 @@ public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner if (_scenarioChangeDebounceTimer == null) { - _scenarioChangeDebounceTimer = new CTimer((o) => DetermineRoomCombinationScenario(), time); + _scenarioChangeDebounceTimer = new Timer(time) { AutoReset = false }; + _scenarioChangeDebounceTimer.Elapsed += async (s, e) => await DetermineRoomCombinationScenario(); + _scenarioChangeDebounceTimer.Start(); } else { - _scenarioChangeDebounceTimer.Reset(time); + _scenarioChangeDebounceTimer.Stop(); + _scenarioChangeDebounceTimer.Interval = time; + _scenarioChangeDebounceTimer.Start(); } } /// /// Determines the current room combination scenario based on the state of the partition sensors /// - private void DetermineRoomCombinationScenario() + private async Task DetermineRoomCombinationScenario() { if (_scenarioChangeDebounceTimer != null) { @@ -250,7 +255,7 @@ public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner if (currentScenario != null) { this.LogInformation("Found combination Scenario {scenarioKey}", currentScenario.Key); - ChangeScenario(currentScenario); + await ChangeScenario(currentScenario); } } diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsNDisplayRoomPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsNDisplayRoomPropertiesConfig.cs index 7b7d43b3..85dcc6ad 100644 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsNDisplayRoomPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Room/Config/EssentialsNDisplayRoomPropertiesConfig.cs @@ -24,18 +24,12 @@ public class EssentialsNDisplayRoomPropertiesConfig : EssentialsConferenceRoomPr [JsonProperty("defaultVideoBehavior")] public string DefaultVideoBehavior { get; set; } - /// - /// Gets or sets the Displays - /// - [JsonProperty("displays")] - public Dictionary Displays { get; set; } /// /// Constructor /// public EssentialsNDisplayRoomPropertiesConfig() { - Displays = new Dictionary(); } } diff --git a/src/PepperDash.Essentials.Core/Timers/CountdownTimer.cs b/src/PepperDash.Essentials.Core/Timers/CountdownTimer.cs index b44005e3..9a6d8c23 100644 --- a/src/PepperDash.Essentials.Core/Timers/CountdownTimer.cs +++ b/src/PepperDash.Essentials.Core/Timers/CountdownTimer.cs @@ -1,30 +1,56 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; +using System.Timers; using PepperDash.Core; using Serilog.Events; namespace PepperDash.Essentials.Core; +/// +/// A class that represents a countdown timer with feedbacks for time remaining, percent, and seconds +/// public class SecondsCountdownTimer: IKeyed { + /// + /// Event triggered when the timer starts. + /// public event EventHandler HasStarted; + /// + /// Event triggered when the timer finishes. + /// public event EventHandler HasFinished; + /// + /// Event triggered when the timer is cancelled. + /// public event EventHandler WasCancelled; + /// public string Key { get; private set; } + /// + /// Indicates whether the timer is currently running + /// public BoolFeedback IsRunningFeedback { get; private set; } bool _isRunning; + /// + /// Feedback for the percentage of time remaining + /// public IntFeedback PercentFeedback { get; private set; } + + /// + /// Feedback for the time remaining in a string format + // public StringFeedback TimeRemainingFeedback { get; private set; } + /// + /// Feedback for the time remaining in seconds + /// public IntFeedback SecondsRemainingFeedback { get; private set; } + /// + /// When true, the timer will count down immediately upon calling Start. When false, the timer will count up, and when Finish is called, it will stop counting and fire the HasFinished event. + /// public bool CountsDown { get; set; } /// @@ -32,10 +58,17 @@ public class SecondsCountdownTimer: IKeyed /// public int SecondsToCount { get; set; } + /// + /// The time at which the timer was started. Used to calculate percent and time remaining. Will be DateTime.MinValue if the timer is not currently running. + /// public DateTime StartTime { get; private set; } + + /// + /// The time at which the timer will finish counting down. Used to calculate percent and time remaining. Will be DateTime.MinValue if the timer is not currently running. + /// public DateTime FinishTime { get; private set; } - private CTimer _secondTimer; + private Timer _secondTimer; /// /// Constructor @@ -88,7 +121,10 @@ public class SecondsCountdownTimer: IKeyed if (_secondTimer != null) _secondTimer.Stop(); - _secondTimer = new CTimer(SecondElapsedTimerCallback, null, 0, 1000); + _secondTimer = new Timer(1000) { AutoReset = true }; + _secondTimer.Elapsed += (s, e) => SecondElapsedTimerCallback(null); + _secondTimer.Start(); + SecondElapsedTimerCallback(null); _isRunning = true; IsRunningFeedback.FireUpdate(); diff --git a/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs b/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs index 14eb3d22..452517b2 100644 --- a/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs +++ b/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Crestron.SimplSharp; +using System.Timers; using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Newtonsoft.Json; @@ -17,9 +17,14 @@ public class RetriggerableTimer : EssentialsDevice { private RetriggerableTimerPropertiesConfig _propertiesConfig; - private CTimer _timer; + private Timer _timer; private long _timerIntervalMs; + /// + /// Constructor for RetriggerableTimer + /// + /// + /// public RetriggerableTimer(string key, DeviceConfig config) : base(key, config.Name) { @@ -32,6 +37,7 @@ public class RetriggerableTimer : EssentialsDevice } } + /// public override bool CustomActivate() { if (_propertiesConfig.StartTimerOnActivation) @@ -53,14 +59,26 @@ public class RetriggerableTimer : EssentialsDevice _timer = null; } + /// + /// Starts the timer with the interval specified in config. When the timer elapses, it executes the action specified in config for the Elapsed event. If the timer is already running, it will reset and start again. + /// When the timer is stopped, it executes the action specified in config for the Stopped event. + /// public void StartTimer() { CleanUpTimer(); Debug.LogMessage(LogEventLevel.Information, this, "Starting Timer"); - _timer = new CTimer(TimerElapsedCallback, GetActionFromConfig(eRetriggerableTimerEvents.Elapsed), _timerIntervalMs, _timerIntervalMs); + var action = GetActionFromConfig(eRetriggerableTimerEvents.Elapsed); + _timer = new Timer(_timerIntervalMs) { AutoReset = true }; + _timer.Elapsed += (s, e) => TimerElapsedCallback(action); + _timer.Start(); } + /// + /// Stops the timer. If the timer is stopped before it elapses, it will execute the action specified in config for the Stopped event. If the timer is not running, this method does nothing. + /// If the timer is running, it will stop the timer and execute the Stopped action from config. If the timer is not running, it will do nothing. + /// If the timer is running and the Stopped action is not specified in config, it will stop the timer and do nothing else. If the timer is running and the Stopped action is specified in config, it will stop the timer and execute the action. If the timer is not running, it will do nothing regardless + /// public void StopTimer() { Debug.LogMessage(LogEventLevel.Information, this, "Stopping Timer"); @@ -81,7 +99,7 @@ public class RetriggerableTimer : EssentialsDevice /// /// Executes the Elapsed action from confing when the timer elapses /// - /// + /// The action to execute when the timer elapses private void TimerElapsedCallback(object action) { Debug.LogMessage(LogEventLevel.Debug, this, "Timer Elapsed. Executing Action"); @@ -127,15 +145,29 @@ public class RetriggerableTimer : EssentialsDevice /// public class RetriggerableTimerPropertiesConfig { + /// + /// When true, the timer will start immediately upon activation. When false, the timer will not start until StartTimer is called. + /// [JsonProperty("startTimerOnActivation")] public bool StartTimerOnActivation { get; set; } + /// + /// The interval at which the timer elapses, in milliseconds. This is required and must be greater than 0. If this value is not set or is less than or equal to 0, the timer will not start and an error will be logged. + /// [JsonProperty("timerIntervalMs")] public long TimerIntervalMs { get; set; } + /// + /// The actions to execute when timer events occur. The key is the type of event, and the value is the action to execute when that event occurs. + /// This is required and must contain at least an action for the Elapsed event. + /// If an action for the Stopped event is not included, then when the timer is stopped, it will simply stop without executing any action. + /// [JsonProperty("events")] public Dictionary Events { get; set; } + /// + /// Constructor + /// public RetriggerableTimerPropertiesConfig() { Events = new Dictionary(); @@ -147,7 +179,14 @@ public class RetriggerableTimerPropertiesConfig /// public enum eRetriggerableTimerEvents { + /// + /// Elapsed event state + /// Elapsed, + + /// + /// Stopped event state + /// Stopped, } @@ -156,11 +195,16 @@ public enum eRetriggerableTimerEvents /// public class RetriggerableTimerFactory : EssentialsDeviceFactory { + /// + /// Constructor for factory + /// + /// public RetriggerableTimerFactory() { TypeNames = new List() { "retriggerabletimer" }; } + /// public override EssentialsDevice BuildDevice(DeviceConfig dc) { Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new RetriggerableTimer Device"); diff --git a/src/PepperDash.Essentials.Core/Touchpanels/Keyboards/HabaneroKeyboardController.cs b/src/PepperDash.Essentials.Core/Touchpanels/Keyboards/HabaneroKeyboardController.cs index ea3dd372..5c70b740 100644 --- a/src/PepperDash.Essentials.Core/Touchpanels/Keyboards/HabaneroKeyboardController.cs +++ b/src/PepperDash.Essentials.Core/Touchpanels/Keyboards/HabaneroKeyboardController.cs @@ -1,10 +1,13 @@ using System; using System.Text; -using Crestron.SimplSharp; +using System.Timers; using Crestron.SimplSharpPro.DeviceSupport; namespace PepperDash.Essentials.Core.Touchpanels.Keyboards; +/// +/// Controller for the Habanero keyboard. This class handles all interaction with the keyboard, including showing/hiding, shift states, and key presses. It exposes a KeyPress event for single key presses, and an OutputFeedback string that contains the full text of what's been entered on the keyboard. +/// public class HabaneroKeyboardController { /// @@ -12,29 +15,57 @@ public class HabaneroKeyboardController /// public event EventHandler KeyPress; + /// + /// The BasicTriList that the keyboard is connected to. This is used for all interaction with the keyboard, including showing/hiding, setting button text/visibility, and handling button presses. + /// public BasicTriList TriList { get; private set; } + /// + /// Feedback for the current text entered on the keyboard. This is updated whenever a key is pressed, and can be used to get the full string of what's been entered on the keyboard. + /// public StringFeedback OutputFeedback { get; private set; } + /// + /// Indicates whether the keyboard is currently visible. This is updated when Show and Hide are called, and can be used to determine the current visibility state of the keyboard. + /// public bool IsVisible { get; private set; } + /// + /// The string displayed on the ".com" button. + /// public string DotComButtonString { get; set; } + /// + /// The text displayed on the "Go" button. + /// public string GoButtonText { get; set; } + /// + /// The text displayed on the secondary button. + /// public string SecondaryButtonText { get; set; } + /// + /// Indicates whether the "Go" button is visible. + /// public bool GoButtonVisible { get; set; } + /// + /// Indicates whether the secondary button is visible. + /// public bool SecondaryButtonVisible { get; set; } int ShiftMode = 0; - + StringBuilder Output; + /// + /// An action that is run when the keyboard is hidden, either by calling the Hide method or by pressing the close button on the keyboard. This can be used to perform any necessary cleanup or state updates when the keyboard is dismissed. + /// + /// public Action HideAction { get; set; } - CTimer BackspaceTimer; + Timer BackspaceTimer; /// /// @@ -88,8 +119,8 @@ public class HabaneroKeyboardController TriList.SetSigTrueAction(2947, () => Press('.')); TriList.SetSigTrueAction(2948, () => Press('@')); TriList.SetSigTrueAction(2949, () => Press(' ')); - TriList.SetSigHeldAction(2950, 500, StartBackspaceRepeat, StopBackspaceRepeat, Backspace); - //TriList.SetSigTrueAction(2950, Backspace); + TriList.SetSigHeldAction(2950, 500, StartBackspaceRepeat, StopBackspaceRepeat, Backspace); + //TriList.SetSigTrueAction(2950, Backspace); TriList.SetSigTrueAction(2951, Shift); TriList.SetSigTrueAction(2952, NumShift); TriList.SetSigTrueAction(2953, Clear); @@ -117,7 +148,7 @@ public class HabaneroKeyboardController TriList.ClearBoolSigAction(i); // run attached actions - if(HideAction != null) + if (HideAction != null) HideAction(); TriList.SetBool(KeyboardVisible, false); @@ -205,28 +236,31 @@ public class HabaneroKeyboardController char Y(int i) { return new char[] { 'y', 'Y', '6', '^' }[i]; } char Z(int i) { return new char[] { 'z', 'Z', ',', ',' }[i]; } - /// - /// Does what it says - /// - void StartBackspaceRepeat() - { - if (BackspaceTimer == null) - { - BackspaceTimer = new CTimer(o => Backspace(), null, 0, 175); - } - } + /// + /// Does what it says + /// + void StartBackspaceRepeat() + { + if (BackspaceTimer == null) + { + BackspaceTimer = new Timer(175) { AutoReset = true }; + BackspaceTimer.Elapsed += (s, e) => Backspace(); + BackspaceTimer.Start(); + Backspace(); + } + } - /// - /// Does what it says - /// - void StopBackspaceRepeat() - { - if (BackspaceTimer != null) - { - BackspaceTimer.Stop(); - BackspaceTimer = null; - } - } + /// + /// Does what it says + /// + void StopBackspaceRepeat() + { + if (BackspaceTimer != null) + { + BackspaceTimer.Stop(); + BackspaceTimer = null; + } + } void Backspace() { @@ -384,7 +418,7 @@ public class HabaneroKeyboardController /// /// 2904 /// - public const uint SecondaryButtonTextJoin = 2904; + public const uint SecondaryButtonTextJoin = 2904; /// /// 2905 /// @@ -413,21 +447,58 @@ public class HabaneroKeyboardController /// public class KeyboardControllerPressEventArgs : EventArgs { + /// + /// The text of the key that was pressed. This will be null if a special key (backspace, clear, go, secondary) was pressed, in which case the SpecialKey property should be checked to determine which key was pressed. + /// public string Text { get; private set; } + + /// + /// If a special key (backspace, clear, go, secondary) was pressed, this property indicates which key it was. If a regular key was pressed, this will be KeyboardSpecialKey.None, and the Text property should be checked for the value of the key press. + /// public KeyboardSpecialKey SpecialKey { get; private set; } + /// + /// Constructor for regular key presses + /// public KeyboardControllerPressEventArgs(string text) { Text = text; } + /// + /// Constructor for special key presses + /// + /// public KeyboardControllerPressEventArgs(KeyboardSpecialKey key) { SpecialKey = key; } } +/// +/// An enum representing special keys on the keyboard that don't have text values, such as backspace, clear, go, and the secondary button. The None value is used for regular key presses that do have text values, and should not be used for special key presses. +/// public enum KeyboardSpecialKey { - None = 0, Backspace, Clear, GoButton, SecondaryButton + /// + /// Indicates that a regular key with a text value was pressed, rather than a special key. When this value is set, the Text property of the KeyboardControllerPressEventArgs should be checked to get the value of the key press. + /// + None = 0, + + /// + /// Indicates that the backspace key was pressed. + /// + Backspace, + /// + /// Indicates that the clear key was pressed. + /// + Clear, + /// + /// Indicates that the go button was pressed. + /// + GoButton, + /// + /// Indicates that the secondary button was pressed. + /// + SecondaryButton } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs b/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs index a9713be2..cc54fd22 100644 --- a/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs +++ b/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs @@ -1,5 +1,5 @@ using System; -using Crestron.SimplSharp; +using System.Timers; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; @@ -84,7 +84,7 @@ namespace PepperDash.Essentials.Core; /// public static BoolOutputSig SetSigHeldAction(this BoolOutputSig sig, uint heldMs, Action heldAction, Action holdReleasedAction, Action releaseAction) { - CTimer heldTimer = null; + Timer heldTimer = null; bool wasHeld = false; return sig.SetBoolSigAction(press => { @@ -92,7 +92,8 @@ namespace PepperDash.Essentials.Core; { wasHeld = false; // Could insert a pressed action here - heldTimer = new CTimer(o => + heldTimer = new Timer(heldMs) { AutoReset = false }; + heldTimer.Elapsed += (s, e) => { // if still held and there's an action if (sig.BoolValue && heldAction != null) @@ -101,7 +102,8 @@ namespace PepperDash.Essentials.Core; // Hold action here heldAction(); } - }, heldMs); + }; + heldTimer.Start(); } else if (!press && !wasHeld) // released, no hold { diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs deleted file mode 100644 index e1ca7102..00000000 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs +++ /dev/null @@ -1,715 +0,0 @@ - - -using System; -using System.Collections.Generic; -using System.Linq; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro.DeviceSupport; -using Newtonsoft.Json; -using PepperDash.Core; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.Bridges; -using PepperDash.Essentials.Core.Config; -using Serilog.Events; - -namespace PepperDash.Essentials.Devices.Common.Cameras; - -/// -/// Camera driver for cameras that use the VISCA control protocol. This driver supports both IP and RS-232 control depending on the communication method passed in. It also supports pan/tilt speed control, presets, and focus control. -/// -public class CameraVisca : CameraBase, IHasCameraPtzControl, ICommunicationMonitor, IHasCameraPresets, IHasPowerControlWithFeedback, IBridgeAdvanced, IHasCameraFocusControl, IHasAutoFocusMode -{ - private readonly CameraViscaPropertiesConfig PropertiesConfig; - - /// - /// Gets or sets the Communication - /// - public IBasicCommunication Communication { get; private set; } - - /// - /// Gets or sets the CommunicationMonitor - /// - public StatusMonitorBase CommunicationMonitor { get; private set; } - - /// - /// Used to store the actions to parse inquiry responses as the inquiries are sent - /// - private readonly CrestronQueue> InquiryResponseQueue; - - /// - /// Camera ID (Default 1) - /// - public byte ID = 0x01; - public byte ResponseID; - - /// - /// Slow speed value for pan movement - /// - public byte PanSpeedSlow = 0x10; - - /// - /// Slow speed value for tilt movement - /// - public byte TiltSpeedSlow = 0x10; - - public byte PanSpeedFast = 0x13; - public byte TiltSpeedFast = 0x13; - - // private bool IsMoving; - private bool IsZooming; - - bool _powerIsOn; - public bool PowerIsOn - { - get - { - return _powerIsOn; - } - private set - { - if (value != _powerIsOn) - { - _powerIsOn = value; - PowerIsOnFeedback.FireUpdate(); - CameraIsOffFeedback.FireUpdate(); - } - } - } - - const byte ZoomInCmd = 0x02; - const byte ZoomOutCmd = 0x03; - const byte ZoomStopCmd = 0x00; - - /// - /// Used to determine when to move the camera at a faster speed if a direction is held - /// - CTimer SpeedTimer; - // TODO: Implment speed timer for PTZ controls - - long FastSpeedHoldTimeMs = 2000; - - byte[] IncomingBuffer = new byte[] { }; - - public BoolFeedback PowerIsOnFeedback { get; private set; } - public CameraVisca(string key, string name, IBasicCommunication comm, CameraViscaPropertiesConfig props) : - base(key, name) - { - InquiryResponseQueue = new CrestronQueue>(15); - - Presets = props.Presets; - - PropertiesConfig = props; - - ID = (byte)(props.Id + 0x80); - ResponseID = (byte)((props.Id * 0x10) + 0x80); - - SetupCameraSpeeds(); - - OutputPorts.Add(new RoutingOutputPort("videoOut", eRoutingSignalType.Video, eRoutingPortConnectionType.None, null, this, true)); - - // Default to all capabilties - Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus; - - Communication = comm; - if (comm is ISocketStatus socket) - { - // This instance uses IP control - socket.ConnectionChange += new EventHandler(Socket_ConnectionChange); - } - else - { - // This instance uses RS-232 control - } - - Communication.BytesReceived += new EventHandler(Communication_BytesReceived); - PowerIsOnFeedback = new BoolFeedback(() => { return PowerIsOn; }); - CameraIsOffFeedback = new BoolFeedback(() => { return !PowerIsOn; }); - - if (props.CommunicationMonitorProperties != null) - { - CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties); - } - else - { - CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 20000, 120000, 300000, "\x81\x09\x04\x00\xFF"); - } - DeviceManager.AddDevice(CommunicationMonitor); - } - - - /// - /// Sets up camera speed values based on config - /// - void SetupCameraSpeeds() - { - if (PropertiesConfig.FastSpeedHoldTimeMs > 0) - { - FastSpeedHoldTimeMs = PropertiesConfig.FastSpeedHoldTimeMs; - } - - if (PropertiesConfig.PanSpeedSlow > 0) - { - PanSpeedSlow = (byte)PropertiesConfig.PanSpeedSlow; - } - if (PropertiesConfig.PanSpeedFast > 0) - { - PanSpeedFast = (byte)PropertiesConfig.PanSpeedFast; - } - - if (PropertiesConfig.TiltSpeedSlow > 0) - { - TiltSpeedSlow = (byte)PropertiesConfig.TiltSpeedSlow; - } - if (PropertiesConfig.TiltSpeedFast > 0) - { - TiltSpeedFast = (byte)PropertiesConfig.TiltSpeedFast; - } - } - - public override bool CustomActivate() - { - Communication.Connect(); - - - CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; - CommunicationMonitor.Start(); - - - CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; - CommunicationMonitor.Start(); - - - CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator); - return true; - } - - /// - /// LinkToApi method - /// - public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) - { - LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge); - } - - void Socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString()); - - if (e.Client.IsConnected) - { - - } - else - { - - } - } - - - void SendBytes(byte[] b) - { - - if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 - Debug.LogMessage(LogEventLevel.Verbose, this, "Sending:{0}", ComTextHelper.GetEscapedText(b)); - - Communication.SendBytes(b); - } - - void Communication_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e) - { - var newBytes = new byte[IncomingBuffer.Length + e.Bytes.Length]; - - try - { - // This is probably not thread-safe buffering - // Append the incoming bytes with whatever is in the buffer - IncomingBuffer.CopyTo(newBytes, 0); - e.Bytes.CopyTo(newBytes, IncomingBuffer.Length); - if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1 - Debug.LogMessage(LogEventLevel.Verbose, this, "Received:{0}", ComTextHelper.GetEscapedText(newBytes)); - - byte[] message = new byte[] { }; - - // Search for the delimiter 0xFF character - for (int i = 0; i < newBytes.Length; i++) - { - if (newBytes[i] == 0xFF) - { - // i will be the index of the delmiter character - message = newBytes.Take(i).ToArray(); - // Skip over what we just took and save the rest for next time - newBytes = newBytes.Skip(i).ToArray(); - } - } - - if (message.Length > 0) - { - // Check for matching ID - if (message[0] != ResponseID) - { - return; - } - - switch (message[1]) - { - case 0x40: - { - // ACK received - Debug.LogMessage(LogEventLevel.Verbose, this, "ACK Received"); - break; - } - case 0x50: - { - - if (message[2] == 0xFF) - { - // Completion received - Debug.LogMessage(LogEventLevel.Verbose, this, "Completion Received"); - } - else - { - // Inquiry response received. Dequeue the next response handler and invoke it - if (InquiryResponseQueue.Count > 0) - { - var inquiryAction = InquiryResponseQueue.Dequeue(); - - inquiryAction.Invoke(message.Skip(2).ToArray()); - } - else - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Response Queue is empty. Nothing to dequeue."); - } - } - - break; - } - case 0x60: - { - // Error message - - switch (message[2]) - { - case 0x01: - { - // Message Length Error - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Message Length Error"); - break; - } - case 0x02: - { - // Syntax Error - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Syntax Error"); - break; - } - case 0x03: - { - // Command Buffer Full - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command Buffer Full"); - break; - } - case 0x04: - { - // Command Cancelled - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command Cancelled"); - break; - } - case 0x05: - { - // No Socket - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: No Socket"); - break; - } - case 0x41: - { - // Command not executable - Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command not executable"); - break; - } - } - break; - } - } - - if (message == new byte[] { ResponseID, 0x50, 0x02, 0xFF }) - { - PowerIsOn = true; - } - else if (message == new byte[] { ResponseID, 0x50, 0x03, 0xFF }) - { - PowerIsOn = false; - } - - } - - } - catch (Exception err) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Error parsing feedback: {0}", err); - } - finally - { - // Save whatever partial message is here - IncomingBuffer = newBytes; - } - } - - /// - /// Sends a pan/tilt command. If the command is not for fastSpeed then it starts a timer to initiate fast speed. - /// - /// - /// - private void SendPanTiltCommand(byte[] cmd, bool fastSpeedEnabled) - { - SendBytes(GetPanTiltCommand(cmd, fastSpeedEnabled)); - - if (!fastSpeedEnabled) - { - if (SpeedTimer != null) - { - StopSpeedTimer(); - } - - // Start the timer to send fast speed if still moving after FastSpeedHoldTime elapses - SpeedTimer = new CTimer((o) => SendPanTiltCommand(GetPanTiltCommand(cmd, true), true), FastSpeedHoldTimeMs); - } - - } - - private void StopSpeedTimer() - { - if (SpeedTimer != null) - { - SpeedTimer.Stop(); - SpeedTimer.Dispose(); - SpeedTimer = null; - } - } - - /// - /// Generates the pan/tilt command with either slow or fast speed - /// - /// - /// - /// - private byte[] GetPanTiltCommand(byte[] cmd, bool fastSpeed) - { - byte panSpeed; - byte tiltSpeed; - - if (!fastSpeed) - { - panSpeed = PanSpeedSlow; - tiltSpeed = TiltSpeedSlow; - } - else - { - panSpeed = PanSpeedFast; - tiltSpeed = TiltSpeedFast; - } - - var temp = new byte[] { ID, 0x01, 0x06, 0x01, panSpeed, tiltSpeed }; - int length = temp.Length + cmd.Length + 1; - - byte[] sum = new byte[length]; - temp.CopyTo(sum, 0); - cmd.CopyTo(sum, temp.Length); - sum[length - 1] = 0xFF; - - return sum; - } - - - void SendPowerQuery() - { - SendBytes(new byte[] { ID, 0x09, 0x04, 0x00, 0xFF }); - InquiryResponseQueue.Enqueue(HandlePowerResponse); - } - - public void PowerOn() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x00, 0x02, 0xFF }); - SendPowerQuery(); - } - - void HandlePowerResponse(byte[] response) - { - switch (response[0]) - { - case 0x02: - { - PowerIsOn = true; - break; - } - case 0x03: - { - PowerIsOn = false; - break; - } - } - } - - public void PowerOff() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x00, 0x03, 0xFF }); - SendPowerQuery(); - } - - public void PowerToggle() - { - if (PowerIsOnFeedback.BoolValue) - PowerOff(); - else - PowerOn(); - } - - public void PanLeft() - { - SendPanTiltCommand(new byte[] { 0x01, 0x03 }, false); - // IsMoving = true; - } - public void PanRight() - { - SendPanTiltCommand(new byte[] { 0x02, 0x03 }, false); - // IsMoving = true; - } - public void PanStop() - { - Stop(); - } - public void TiltDown() - { - SendPanTiltCommand(new byte[] { 0x03, 0x02 }, false); - // IsMoving = true; - } - public void TiltUp() - { - SendPanTiltCommand(new byte[] { 0x03, 0x01 }, false); - // IsMoving = true; - } - public void TiltStop() - { - Stop(); - } - - private void SendZoomCommand(byte cmd) - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x07, cmd, 0xFF }); - } - - - public void ZoomIn() - { - SendZoomCommand(ZoomInCmd); - IsZooming = true; - } - public void ZoomOut() - { - SendZoomCommand(ZoomOutCmd); - IsZooming = true; - } - public void ZoomStop() - { - Stop(); - } - - public void Stop() - { - if (IsZooming) - { - SendZoomCommand(ZoomStopCmd); - IsZooming = false; - } - else - { - StopSpeedTimer(); - SendPanTiltCommand(new byte[] { 0x03, 0x03 }, false); - // IsMoving = false; - } - } - public void PositionHome() - { - SendBytes(new byte[] { ID, 0x01, 0x06, 0x02, PanSpeedFast, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }); - SendBytes(new byte[] { ID, 0x01, 0x04, 0x47, 0x00, 0x00, 0x00, 0x00, 0xFF }); - } - public void RecallPreset(int presetNumber) - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x3F, 0x02, (byte)presetNumber, 0xFF }); - } - public void SavePreset(int presetNumber) - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x3F, 0x01, (byte)presetNumber, 0xFF }); - } - - #region IHasCameraPresets Members - - public event EventHandler PresetsListHasChanged; - - /// - /// Raises the PresetsListHasChanged event - /// - protected void OnPresetsListHasChanged() - { - var handler = PresetsListHasChanged; - if (handler == null) - return; - - handler.Invoke(this, EventArgs.Empty); - } - - public List Presets { get; private set; } - - public void PresetSelect(int preset) - { - RecallPreset(preset); - } - - public void PresetStore(int preset, string description) - { - SavePreset(preset); - } - - - #endregion - - #region IHasCameraFocusControl Members - - public void FocusNear() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x03, 0xFF }); - } - - public void FocusFar() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x02, 0xFF }); - } - - public void FocusStop() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x00, 0xFF }); - } - - public void TriggerAutoFocus() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x18, 0x01, 0xFF }); - SendAutoFocusQuery(); - } - - #endregion - - #region IHasAutoFocus Members - - public void SetFocusModeAuto() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x02, 0xFF }); - SendAutoFocusQuery(); - } - - public void SetFocusModeManual() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x03, 0xFF }); - SendAutoFocusQuery(); - } - - public void ToggleFocusMode() - { - SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x10, 0xFF }); - SendAutoFocusQuery(); - } - - #endregion - - void SendAutoFocusQuery() - { - SendBytes(new byte[] { ID, 0x09, 0x04, 0x38, 0xFF }); - InquiryResponseQueue.Enqueue(HandleAutoFocusResponse); - } - - void HandleAutoFocusResponse(byte[] response) - { - switch (response[0]) - { - case 0x02: - { - // Auto Mode - PowerIsOn = true; - break; - } - case 0x03: - { - // Manual Mode - PowerIsOn = false; - break; - } - } - } - - #region IHasCameraOff Members - - public BoolFeedback CameraIsOffFeedback { get; private set; } - - - public void CameraOff() - { - PowerOff(); - } - - #endregion -} - -public class CameraViscaFactory : EssentialsDeviceFactory -{ - public CameraViscaFactory() - { - TypeNames = new List() { "cameravisca" }; - } - - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new CameraVisca Device"); - var comm = CommFactory.CreateCommForDevice(dc); - var props = Newtonsoft.Json.JsonConvert.DeserializeObject( - dc.Properties.ToString()); - return new Cameras.CameraVisca(dc.Key, dc.Name, comm, props); - } -} - - -public class CameraViscaPropertiesConfig : CameraPropertiesConfig -{ - /// - /// Control ID of the camera (1-7) - /// - [JsonProperty("id")] - public uint Id { get; set; } - - /// - /// Slow Pan speed (0-18) - /// - [JsonProperty("panSpeedSlow")] - public uint PanSpeedSlow { get; set; } - - /// - /// Fast Pan speed (0-18) - /// - [JsonProperty("panSpeedFast")] - public uint PanSpeedFast { get; set; } - - /// - /// Slow tilt speed (0-18) - /// - [JsonProperty("tiltSpeedSlow")] - public uint TiltSpeedSlow { get; set; } - - /// - /// Fast tilt speed (0-18) - /// - [JsonProperty("tiltSpeedFast")] - public uint TiltSpeedFast { get; set; } - - /// - /// Time a button must be held before fast speed is engaged (Milliseconds) - /// - [JsonProperty("fastSpeedHoldTimeMs")] - public uint FastSpeedHoldTimeMs { get; set; } - -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/CameraSelectedEventArgs.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/CameraSelectedEventArgs.cs index b96a9b3f..d669a567 100644 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/CameraSelectedEventArgs.cs +++ b/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/CameraSelectedEventArgs.cs @@ -1,32 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials.Devices.Common.Cameras { - /// - /// Event arguments for the CameraSelected event - /// - [Obsolete("Use CameraSelectedEventArgs instead. This class will be removed in a future version")] - public class CameraSelectedEventArgs : EventArgs - { - /// - /// Gets or sets the SelectedCamera - /// - public CameraBase SelectedCamera { get; private set; } - - /// - /// Constructor for CameraSelectedEventArgs - /// - /// - public CameraSelectedEventArgs(CameraBase camera) - { - SelectedCamera = camera; - } - } - /// /// Event arguments for the CameraSelected event /// diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCameras.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCameras.cs deleted file mode 100644 index 5a5962f8..00000000 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCameras.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Newtonsoft.Json; -using PepperDash.Core; -using PepperDash.Essentials.Core; -using System; -using System.Collections.Generic; - -namespace PepperDash.Essentials.Devices.Common.Cameras -{ - /// - /// Interface for devices that have cameras - /// - [Obsolete("Use IHasCamerasWithControls instead. This interface will be removed in a future version")] - public interface IHasCameras : IKeyName - { - /// - /// Event that is raised when a camera is selected - /// - event EventHandler CameraSelected; - - /// - /// List of cameras on the device. This should be a list of CameraBase objects - /// - List Cameras { get; } - - /// - /// The currently selected camera. This should be a CameraBase object - /// - CameraBase SelectedCamera { get; } - - /// - /// Feedback that indicates the currently selected camera - /// - StringFeedback SelectedCameraFeedback { get; } - - /// - /// Selects a camera from the list of available cameras based on the provided key. - /// - /// The unique identifier or name of the camera to select. - void SelectCamera(string key); - } -} diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCamerasWithControls.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCamerasWithControls.cs index f7aff6d2..e4b76572 100644 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCamerasWithControls.cs +++ b/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCamerasWithControls.cs @@ -8,7 +8,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras /// /// Interface for devices that have cameras with controls /// - public interface IHasCamerasWithControls : IKeyName, IKeyed + public interface IHasCamerasWithControls : IKeyName { /// /// List of cameras on the device. This should be a list of IHasCameraControls objects diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCodecCameras.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCodecCameras.cs index 01cdf24e..32c312f7 100644 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCodecCameras.cs +++ b/src/PepperDash.Essentials.Devices.Common/Cameras/Interfaces/IHasCodecCameras.cs @@ -5,7 +5,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras /// /// Defines the contract for IHasCodecCameras /// - public interface IHasCodecCameras : IHasCameras, IHasFarEndCameraControl + public interface IHasCodecCameras : IHasCamerasWithControls, IHasFarEndCameraControl { } diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/CodecScheduleAwareness.cs b/src/PepperDash.Essentials.Devices.Common/Codec/CodecScheduleAwareness.cs index 4eea3d2a..777474db 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/CodecScheduleAwareness.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/CodecScheduleAwareness.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -using Crestron.SimplSharp; +using System.Timers; using PepperDash.Core; using Serilog.Events; @@ -56,16 +56,14 @@ namespace PepperDash.Essentials.Devices.Common.Codec } } - private readonly CTimer _scheduleChecker; + private Timer _scheduleChecker; /// /// Initializes a new instance of the CodecScheduleAwareness class with default poll time /// public CodecScheduleAwareness() { - Meetings = new List(); - - _scheduleChecker = new CTimer(CheckSchedule, null, 1000, 1000); + Init(1000); } /// @@ -73,10 +71,17 @@ namespace PepperDash.Essentials.Devices.Common.Codec /// /// The poll time in milliseconds for checking schedule changes public CodecScheduleAwareness(long pollTime) + { + Init(pollTime); + } + + private void Init(long pollTime) { Meetings = new List(); - _scheduleChecker = new CTimer(CheckSchedule, null, pollTime, pollTime); + _scheduleChecker = new Timer(pollTime) { AutoReset = true }; + _scheduleChecker.Elapsed += (s, e) => CheckSchedule(null); + _scheduleChecker.Start(); } /// diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs index 445cabcf..5a018a84 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Crestron.SimplSharp; +using System.Timers; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Core; @@ -207,22 +207,26 @@ public class BasicIrDisplay : DisplayBase, IBasicVolumeControls, IBridgeAdvanced { _IsWarmingUp = true; IsWarmingUpFeedback.FireUpdate(); - new CTimer(o => + var t = new Timer(10000) { AutoReset = false }; + t.Elapsed += (s, e) => { _IsWarmingUp = false; IsWarmingUpFeedback.FireUpdate(); - }, 10000); + }; + t.Start(); } void StartCoolingTimer() { _IsCoolingDown = true; IsCoolingDownFeedback.FireUpdate(); - new CTimer(o => + var t = new Timer(7000) { AutoReset = false }; + t.Elapsed += (s, e) => { _IsCoolingDown = false; IsCoolingDownFeedback.FireUpdate(); - }, 7000); + }; + t.Start(); } #region IRoutingSink Members diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs index 5f35e90a..89f6f415 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Crestron.SimplSharp; +using System.Timers; using Crestron.SimplSharpPro.DeviceSupport; using Newtonsoft.Json; using PepperDash.Core; @@ -103,12 +103,12 @@ public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, /// /// Timer used for managing display warmup timing. /// - protected CTimer WarmupTimer; + protected Timer WarmupTimer; /// /// Timer used for managing display cooldown timing. /// - protected CTimer CooldownTimer; + protected Timer CooldownTimer; #region IRoutingInputs Members diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs index ead4c0eb..a9d9a261 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Crestron.SimplSharp; +using System.Timers; using Crestron.SimplSharpPro.DeviceSupport; using PepperDash.Core; using PepperDash.Essentials.Core; @@ -17,12 +17,14 @@ namespace PepperDash.Essentials.Devices.Common.Displays; /// public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs, IRoutingSinkWithSwitchingWithInputPort, IHasPowerControlWithFeedback { + /// public ISelectableItems Inputs { get; private set; } bool _PowerIsOn; bool _IsWarmingUp; bool _IsCoolingDown; + /// protected override Func PowerIsOnFeedbackFunc { get @@ -33,6 +35,8 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA }; } } + + /// protected override Func IsCoolingDownFeedbackFunc { get @@ -43,6 +47,8 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA }; } } + + /// protected override Func IsWarmingUpFeedbackFunc { get @@ -53,13 +59,21 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA }; } } + + /// protected override Func CurrentInputFeedbackFunc { get { return () => Inputs.CurrentItem; } } int VolumeHeldRepeatInterval = 200; ushort VolumeInterval = 655; ushort _FakeVolumeLevel = 31768; bool _IsMuted; + Timer _volumeUpTimer; + Timer _volumeDownTimer; + /// + /// Constructor for MockDisplay + /// + /// public MockDisplay(string key, string name) : base(key, name) { @@ -108,16 +122,19 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA _IsWarmingUp = true; IsWarmingUpFeedback.InvokeFireUpdate(); // Fake power-up cycle - WarmupTimer = new CTimer(o => + WarmupTimer = new Timer(WarmupTime) { AutoReset = false }; + WarmupTimer.Elapsed += (s, e) => { _IsWarmingUp = false; _PowerIsOn = true; IsWarmingUpFeedback.InvokeFireUpdate(); PowerIsOnFeedback.InvokeFireUpdate(); - }, WarmupTime); + }; + WarmupTimer.Start(); } } + /// public override void PowerOff() { // If a display has unreliable-power off feedback, just override this and @@ -127,17 +144,20 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA _IsCoolingDown = true; IsCoolingDownFeedback.InvokeFireUpdate(); // Fake cool-down cycle - CooldownTimer = new CTimer(o => + CooldownTimer = new Timer(CooldownTime) { AutoReset = false }; + CooldownTimer.Elapsed += (s, e) => { Debug.LogMessage(LogEventLevel.Verbose, "Cooldown timer ending", this); _IsCoolingDown = false; IsCoolingDownFeedback.InvokeFireUpdate(); _PowerIsOn = false; PowerIsOnFeedback.InvokeFireUpdate(); - }, CooldownTime); + }; + CooldownTimer.Start(); } } + /// public override void PowerToggle() { if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue) @@ -146,6 +166,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA PowerOn(); } + /// public override void ExecuteSwitch(object selector) { try @@ -184,6 +205,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA } } + /// public void SetInput(string selector) { ISelectableItem currentInput = null; @@ -206,6 +228,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA #region IBasicVolumeWithFeedback Members + /// public IntFeedback VolumeLevelFeedback { get; private set; } /// @@ -245,32 +268,44 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA #region IBasicVolumeControls Members + /// public void VolumeUp(bool pressRelease) { - //while (pressRelease) - //{ - Debug.LogMessage(LogEventLevel.Verbose, this, "Volume Down {0}", pressRelease); if (pressRelease) { - var newLevel = _FakeVolumeLevel + VolumeInterval; - SetVolume((ushort)newLevel); - CrestronEnvironment.Sleep(VolumeHeldRepeatInterval); + SetVolume((ushort)(_FakeVolumeLevel + VolumeInterval)); + if (_volumeUpTimer == null) + { + _volumeUpTimer = new Timer(VolumeHeldRepeatInterval) { AutoReset = true }; + _volumeUpTimer.Elapsed += (s, e) => SetVolume((ushort)(_FakeVolumeLevel + VolumeInterval)); + _volumeUpTimer.Start(); + } + } + else + { + _volumeUpTimer?.Stop(); + _volumeUpTimer = null; } - //} } + /// public void VolumeDown(bool pressRelease) { - //while (pressRelease) - //{ - Debug.LogMessage(LogEventLevel.Verbose, this, "Volume Up {0}", pressRelease); if (pressRelease) { - var newLevel = _FakeVolumeLevel - VolumeInterval; - SetVolume((ushort)newLevel); - CrestronEnvironment.Sleep(VolumeHeldRepeatInterval); + SetVolume((ushort)(_FakeVolumeLevel - VolumeInterval)); + if (_volumeDownTimer == null) + { + _volumeDownTimer = new Timer(VolumeHeldRepeatInterval) { AutoReset = true }; + _volumeDownTimer.Elapsed += (s, e) => SetVolume((ushort)(_FakeVolumeLevel - VolumeInterval)); + _volumeDownTimer.Start(); + } + } + else + { + _volumeDownTimer?.Stop(); + _volumeDownTimer = null; } - //} } /// @@ -284,6 +319,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA #endregion + /// public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) { LinkDisplayToApi(this, trilist, joinStart, joinMapKey, bridge); @@ -303,6 +339,7 @@ public class MockDisplayFactory : EssentialsDeviceFactory TypeNames = new List() { "mockdisplay", "mockdisplay2" }; } + /// public override EssentialsDevice BuildDevice(DeviceConfig dc) { Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Mock Display Device"); diff --git a/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs b/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs index c1e613e2..d87d29fb 100644 --- a/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs +++ b/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Crestron.SimplSharp; +using System.Timers; using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; @@ -84,7 +84,9 @@ public class RelayControlledShade : ShadeBase, IShadesOpenCloseStop void PulseOutput(ISwitchedOutput output, int pulseTime) { output.On(); - CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime); + var pulseTimer = new Timer(pulseTime) { AutoReset = false }; + pulseTimer.Elapsed += (s, e) => output.Off(); + pulseTimer.Start(); } /// diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs index e9f7908c..7b8f73a3 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Crestron.SimplSharp; +using System.Timers; using Crestron.SimplSharpPro.DeviceSupport; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -170,12 +170,16 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec OnCallStatusChange(call); //ActiveCallCountFeedback.FireUpdate(); // Simulate 2-second ring, then connecting, then connected - new CTimer(o => + var dialTimer = new Timer(2000) { AutoReset = false }; + dialTimer.Elapsed += (s, e) => { call.Type = eCodecCallType.Video; SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); - new CTimer(oo => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call), 1000); - }, 2000); + var connectTimer = new Timer(1000) { AutoReset = false }; + connectTimer.Elapsed += (ss, ee) => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call); + connectTimer.Start(); + }; + dialTimer.Start(); } /// @@ -188,12 +192,16 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec //ActiveCallCountFeedback.FireUpdate(); // Simulate 2-second ring, then connecting, then connected - new CTimer(o => + var dialMeetingTimer = new Timer(2000) { AutoReset = false }; + dialMeetingTimer.Elapsed += (s, e) => { call.Type = eCodecCallType.Video; SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); - new CTimer(oo => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call), 1000); - }, 2000); + var connectTimer = new Timer(1000) { AutoReset = false }; + connectTimer.Elapsed += (ss, ee) => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call); + connectTimer.Start(); + }; + dialMeetingTimer.Start(); } @@ -226,7 +234,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec { Debug.LogMessage(LogEventLevel.Debug, this, "AcceptCall"); SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); - new CTimer(o => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call), 1000); + var acceptTimer = new Timer(1000) { AutoReset = false }; + acceptTimer.Elapsed += (s, e) => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call); + acceptTimer.Start(); // should already be in active list } @@ -624,7 +634,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec SupportsCameraOff = false; - Cameras = new List(); + Cameras = new List(); var internalCamera = new MockVCCamera(Key + "-camera1", "Near End", this); @@ -679,19 +689,19 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// /// CameraSelected event. Fired when a camera is selected /// - public event EventHandler CameraSelected; + public event EventHandler> CameraSelected; /// /// Gets the list of cameras associated with this codec /// - public List Cameras { get; private set; } + public List Cameras { get; private set; } - private CameraBase _selectedCamera; + private IHasCameraControls _selectedCamera; /// /// Returns the selected camera /// - public CameraBase SelectedCamera + public IHasCameraControls SelectedCamera { get { @@ -702,7 +712,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec _selectedCamera = value; SelectedCameraFeedback.FireUpdate(); ControllingFarEndCameraFeedback.FireUpdate(); - CameraSelected?.Invoke(this, new CameraSelectedEventArgs(SelectedCamera)); + CameraSelected?.Invoke(this, new CameraSelectedEventArgs(SelectedCamera)); } } diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index e2cc5e67..8ed740d3 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -24,10 +24,19 @@ using System.Threading.Tasks; namespace PepperDash.Essentials.Devices.Common.VideoCodec; +/// +/// Base class for video codecs. Contains common properties, methods, and feedback for video codecs. +/// Also contains the logic to link commonly implemented interfaces to the API bridge. +/// public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs, IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode { private const int XSigEncoding = 28591; + + /// + /// Maximum number of participants to display in the participant list. + /// This is used to limit the number of XSigs that need to be updated and processed on the control system when there are many participants in a call. + /// protected const int MaxParticipants = 50; private readonly byte[] _clearBytes = XSigHelpers.ClearOutputs(); @@ -35,6 +44,10 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu private readonly BasicTriList _directoryTrilist; private readonly VideoCodecControllerJoinMap _directoryJoinmap; + /// + /// Time format specifier for any time-related XSigs. + /// This should be set by the inheriting class based on the expected format of the codec (e.g. "hh:mm:ss" or "mm:ss") + /// protected string _timeFormatSpecifier; /// @@ -56,7 +69,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu SharingSourceFeedback = new StringFeedback("sharingSource", SharingSourceFeedbackFunc); SharingContentIsOnFeedback = new BoolFeedback("sharingContentIsOn", SharingContentIsOnFeedbackFunc); - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set MeetingsToDisplayFeedback = new IntFeedback(() => MeetingsToDisplay); InputPorts = new RoutingPortCollection(); @@ -110,7 +122,10 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu /// public bool ShowSelfViewByDefault { get; protected set; } + /// public bool SupportsCameraOff { get; protected set; } + + /// public bool SupportsCameraAutoMode { get; protected set; } /// @@ -282,6 +297,8 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu /// Sends DTMF tones /// public abstract void SendDtmf(string s); + + /// public virtual void SendDtmf(string s, CodecActiveCallItem call) { } #endregion @@ -746,7 +763,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu { if (!args.DeviceOnLine) return; - // TODO [ ] Issue #868 trilist.SetString(joinMap.CurrentParticipants.JoinNumber, "\xFC"); UpdateParticipantsXSig(codec, trilist, joinMap); }; @@ -974,10 +990,8 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu { if (!args.DeviceOnLine) return; - // TODO [ ] Issue #868 trilist.SetString(joinMap.Schedule.JoinNumber, "\xFC"); UpdateMeetingsList(codec, trilist, joinMap); - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set MeetingsToDisplayFeedback.LinkInputSig(trilist.UShortInput[joinMap.MeetingsToDisplay.JoinNumber]); }; } @@ -1006,16 +1020,14 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu { if (!args.DeviceOnLine) return; - // TODO [ ] Issue #868 trilist.SetString(joinMap.Schedule.JoinNumber, "\xFC"); UpdateMeetingsListXSig(_currentMeetings); }; } - - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set private int _meetingsToDisplay = 3; - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + + /// protected int MeetingsToDisplay { get { return _meetingsToDisplay; } @@ -1026,12 +1038,13 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu } } - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set + + + /// public IntFeedback MeetingsToDisplayFeedback { get; set; } private string UpdateMeetingsListXSig(List meetings) { - // TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set //const int _meetingsToDisplay = 3; const int maxDigitals = 2; const int maxStrings = 7; @@ -1291,7 +1304,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu Debug.LogMessage(LogEventLevel.Verbose, this, "Creating XSIG token array with size {0}", maxMethods * offset); - // TODO: Add code to generate XSig data foreach (var method in contact.ContactMethods) { if (arrayIndex >= maxMethods * offset) @@ -1498,7 +1510,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu { if (!args.DeviceOnLine) return; - // TODO [ ] #983 Debug.LogMessage(LogEventLevel.Information, this, "LinkVideoCodecCallControlsToApi: device is {0}, IsInCall {1}", args.DeviceOnLine ? "online" : "offline", IsInCall); trilist.SetBool(joinMap.HookState.JoinNumber, IsInCall); trilist.SetString(joinMap.CurrentCallData.JoinNumber, "\xFC"); @@ -1531,12 +1542,10 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, call.Direction.ToString()); tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, call.Type.ToString()); tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, call.Status.ToString()); - if (call.Duration != null) - { - // May need to verify correct string format here - var dur = string.Format("{0:c}", call.Duration); - tokenArray[arrayIndex + 6] = new XSigSerialToken(stringIndex + 6, dur); - } + + // May need to verify correct string format here + var dur = string.Format("{0:c}", call.Duration); + tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, dur); arrayIndex += offset; stringIndex += maxStrings; @@ -1879,7 +1888,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu { if (!args.DeviceOnLine) return; - // TODO [ ] Issue #868 trilist.SetString(joinMap.CameraPresetNames.JoinNumber, "\xFC"); SetCameraPresetNames(presetCodec.NearEndPresets); }; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasMessenger.cs deleted file mode 100644 index 00c2c2fe..00000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasMessenger.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Newtonsoft.Json; -using PepperDash.Essentials.Devices.Common.Cameras; -using System; -using System.Collections.Generic; - -namespace PepperDash.Essentials.AppServer.Messengers -{ - /// - /// Messenger for devices that implement the IHasCameras interface. - /// - [Obsolete("Use IHasCamerasWithControlsMessenger instead. This class will be removed in a future version")] - public class IHasCamerasMessenger : MessengerBase - { - /// - /// Device being bridged that implements IHasCameras interface. - /// - public IHasCameras CameraController { get; private set; } - - /// - /// Messenger for devices that implement IHasCameras interface. - /// - /// - /// - /// - /// - public IHasCamerasMessenger(string key, string messagePath, IHasCameras cameraController) - : base(key, messagePath, cameraController) - { - CameraController = cameraController ?? throw new ArgumentNullException("cameraController"); - CameraController.CameraSelected += CameraController_CameraSelected; - } - - private void CameraController_CameraSelected(object sender, CameraSelectedEventArgs e) - { - PostStatusMessage(new IHasCamerasStateMessage - { - SelectedCamera = e.SelectedCamera - }); - } - - /// - /// Registers the actions for this messenger. - /// - /// - protected override void RegisterActions() - { - base.RegisterActions(); - - AddAction("/fullStatus", (id, context) => SendFullStatus(id)); - - AddAction("/cameraListStatus", (id, content) => SendFullStatus(id)); - - AddAction("/selectCamera", (id, content) => - { - var cameraKey = content?.ToObject(); - - if (!string.IsNullOrEmpty(cameraKey)) - { - CameraController.SelectCamera(cameraKey); - } - else - { - throw new ArgumentException("Content must be a string representing the camera key"); - } - }); - } - - private void SendFullStatus(string clientId) - { - var state = new IHasCamerasStateMessage - { - CameraList = CameraController.Cameras, - SelectedCamera = CameraController.SelectedCamera - }; - - PostStatusMessage(state, clientId); - } - - - } - - /// - /// State message for devices that implement the IHasCameras interface. - /// - public class IHasCamerasStateMessage : DeviceStateMessageBase - { - /// - /// List of cameras available in the device. - /// - [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] - public List CameraList { get; set; } - - /// - /// The currently selected camera on the device. - /// - [JsonProperty("selectedCamera", NullValueHandling = NullValueHandling.Ignore)] - public CameraBase SelectedCamera { get; set; } - - } -} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index e634631a..9d22803d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -285,7 +285,6 @@ namespace PepperDash.Essentials.AppServer.Messengers { try { - //Debug.Console(2, this, "*********************Setting DeviceStateMessageProperties on MobileControlResponseMessage"); deviceState.SetInterfaces(_deviceInterfaces); deviceState.Key = _device.Key; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs index 17eb2656..6256142a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Crestron.SimplSharp; +using System.Timers; using Newtonsoft.Json.Linq; using PepperDash.Core; @@ -13,7 +13,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { private const long ButtonHeartbeatInterval = 1000; - private static readonly Dictionary _pushedActions = new Dictionary(); + private static readonly Dictionary _pushedActions = new Dictionary(); private static readonly Dictionary>> _pushedActionHandlers; @@ -31,7 +31,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { Debug.LogDebug("Attempting to add timer for {deviceKey}", deviceKey); - if (_pushedActions.TryGetValue(deviceKey, out CTimer cancelTimer)) + if (_pushedActions.TryGetValue(deviceKey, out Timer cancelTimer)) { Debug.LogDebug("Timer for {deviceKey} already exists", deviceKey); return; @@ -41,14 +41,16 @@ namespace PepperDash.Essentials.AppServer.Messengers action(true); - cancelTimer = new CTimer(o => + cancelTimer = new Timer(ButtonHeartbeatInterval) { AutoReset = false }; + cancelTimer.Elapsed += (s, e) => { Debug.LogDebug("Timer expired for {deviceKey}", deviceKey); action(false); _pushedActions.Remove(deviceKey); - }, ButtonHeartbeatInterval); + }; + cancelTimer.Start(); _pushedActions.Add(deviceKey, cancelTimer); } @@ -57,7 +59,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { Debug.LogDebug("Attempting to reset timer for {deviceKey}", deviceKey); - if (!_pushedActions.TryGetValue(deviceKey, out CTimer cancelTimer)) + if (!_pushedActions.TryGetValue(deviceKey, out Timer cancelTimer)) { Debug.LogDebug("Timer for {deviceKey} not found", deviceKey); return; @@ -65,14 +67,16 @@ namespace PepperDash.Essentials.AppServer.Messengers Debug.LogDebug("Resetting timer for {deviceKey} with due time {dueTime}", deviceKey, ButtonHeartbeatInterval); - cancelTimer.Reset(ButtonHeartbeatInterval); + cancelTimer.Stop(); + cancelTimer.Interval = ButtonHeartbeatInterval; + cancelTimer.Start(); } private static void StopTimer(string deviceKey, Action action) { Debug.LogDebug("Attempting to stop timer for {deviceKey}", deviceKey); - if (!_pushedActions.TryGetValue(deviceKey, out CTimer cancelTimer)) + if (!_pushedActions.TryGetValue(deviceKey, out Timer cancelTimer)) { Debug.LogDebug("Timer for {deviceKey} not found", deviceKey); return; @@ -85,6 +89,11 @@ namespace PepperDash.Essentials.AppServer.Messengers _pushedActions.Remove(deviceKey); } + /// + /// Gets the handler for a given press and hold message type + /// + /// The press and hold message type. + /// The handler for the specified message type. public static Action> GetPressAndHoldHandler(string value) { Debug.LogDebug("Getting press and hold handler for {value}", value); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs index e4a5d2e7..829f66ee 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs @@ -33,7 +33,7 @@ namespace PepperDash.Essentials.AppServer.Messengers protected override void RegisterActions() { - Debug.Console(2, "********** Direct Route Messenger CustomRegisterWithAppServer **********"); + this.LogDebug("Direct Route Messenger CustomRegisterWithAppServer **********"); //Audio source @@ -81,7 +81,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { var b = content.ToObject>(); - Debug.Console(1, "Current Sharing Mode: {2}\r\nadvanced sharing mode: {0} join number: {1}", b.Value, + this.LogDebug("Current Sharing Mode: {2}\r\nadvanced sharing mode: {0} join number: {1}", b.Value, JoinMap.AdvancedSharingModeOn.JoinNumber, _eisc.BooleanOutput[JoinMap.AdvancedSharingModeOn.JoinNumber].BoolValue); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs index 6c5857e9..0dc713bb 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs @@ -255,10 +255,10 @@ namespace PepperDash.Essentials.AppServer.Messengers _eisc.SetUshort(JoinMap.DirectorySelectRow.JoinNumber, u); _eisc.PulseBool(JoinMap.DirectoryLineSelected.JoinNumber); } - catch (Exception) + catch (Exception e) { - Debug.Console(1, this, Debug.ErrorLogLevel.Warning, - "/directoryById request contains non-numeric ID incompatible with SIMPL bridge"); + this.LogException(e,"directoryById request contains non-numeric ID incompatible with SIMPL bridge: {0}", e.Message); + this.LogVerbose("Stack Trace:\r{0}", e.StackTrace); } }); AddAction("/directorySelectContact", (id, content) => @@ -270,9 +270,10 @@ namespace PepperDash.Essentials.AppServer.Messengers _eisc.SetUshort(JoinMap.DirectorySelectRow.JoinNumber, u); _eisc.PulseBool(JoinMap.DirectoryLineSelected.JoinNumber); } - catch + catch (Exception e) { - Debug.Console(2, this, "Error parsing contact from {0} for path /directorySelectContact", s); + this.LogException(e, "Error parsing contact from {0} for path /directorySelectContact", s.Value); + this.LogVerbose("Stack Trace:\r{0}", e.StackTrace); } }); AddAction("/directoryDialContact", diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs index 857bfa0f..6cf3a5ad 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs @@ -40,7 +40,6 @@ namespace PepperDash.Essentials.AppServer.Messengers { if (e.ProgramInfo != null) { - //Debug.Console(1, "Posting Status Message: {0}", e.ProgramInfo.ToString()); PostStatusMessage(JToken.FromObject(e.ProgramInfo) ); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs index b4f28db4..28f1999f 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs @@ -431,7 +431,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } - private void CameraCodec_CameraSelected(object sender, CameraSelectedEventArgs e) + private void CameraCodec_CameraSelected(object sender, CameraSelectedEventArgs e) { try { @@ -449,7 +449,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// private void MapCameraActions() { - if (Codec is IHasCameras cameraCodec && cameraCodec.SelectedCamera != null) + if (Codec is IHasCamerasWithControls cameraCodec && cameraCodec.SelectedCamera != null) { RemoveAction("/cameraUp"); RemoveAction("/cameraDown"); @@ -764,7 +764,7 @@ namespace PepperDash.Essentials.AppServer.Messengers status.ShowSelfViewByDefault = Codec.ShowSelfViewByDefault; status.SupportsAdHocMeeting = Codec is IHasStartMeeting; status.HasRecents = Codec is IHasCallHistory; - status.HasCameras = Codec is IHasCameras; + status.HasCameras = Codec is IHasCamerasWithControls; status.Presets = GetCurrentPresets(); status.IsZoomRoom = codecType.GetInterface("IHasZoomRoomLayouts") != null; status.ReceivingContent = Codec is IHasFarEndContentStatus && (Codec as IHasFarEndContentStatus).ReceivingContent.BoolValue; @@ -899,12 +899,15 @@ namespace PepperDash.Essentials.AppServer.Messengers { camera.Name = camerasCodec.SelectedCamera.Name; - camera.Capabilities = new CameraCapabilities() + if(camerasCodec.SelectedCamera is IHasCameraPtzControl cameraControls) { - CanPan = camerasCodec.SelectedCamera.CanPan, - CanTilt = camerasCodec.SelectedCamera.CanTilt, - CanZoom = camerasCodec.SelectedCamera.CanZoom, - CanFocus = camerasCodec.SelectedCamera.CanFocus, + camera.Capabilities = new CameraCapabilities() + { + CanPan = cameraControls is IHasCameraPanControl, + CanTilt = cameraControls is IHasCameraTiltControl, + CanZoom = cameraControls is IHasCameraZoomControl, + CanFocus = cameraControls is IHasCameraFocusControl, + }; }; } @@ -1084,7 +1087,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// Gets or sets the Cameras /// [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] - public List Cameras { get; set; } + public List Cameras { get; set; } /// diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 7d90b751..47bdcfe8 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Timers; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.Net.Http; @@ -186,9 +187,9 @@ namespace PepperDash.Essentials /// public DateTime LastAckMessage => _lastAckMessage; - private CTimer _pingTimer; + private Timer _pingTimer; - private CTimer _serverReconnectTimer; + private Timer _serverReconnectTimer; private LogLevel _wsLogLevel = LogLevel.Error; /// @@ -769,21 +770,6 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IHasCurrentSourceInfoChange csiChange) - { - this.LogVerbose("Adding IHasCurrentSourceInfoMessenger for {deviceKey}", device.Key); - - var messenger = new IHasCurrentSourceInfoMessenger( - $"{device.Key}-currentSource-{Key}", - $"/device/{device.Key}", - csiChange - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - if (device is ICurrentSources currentSources) { this.LogVerbose("Adding CurrentSourcesMessenger for {deviceKey}", device.Key); @@ -993,19 +979,6 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IHasCameras cameras) - { - this.LogVerbose("Adding IHasCamerasMessenger for {deviceKey}", device.Key - ); - var messenger = new IHasCamerasMessenger( - $"{device.Key}-cameras-{Key}", - $"/device/{device.Key}", - cameras - ); - AddDefaultDeviceMessenger(messenger); - messengerAdded = true; - } - if (device is IHasCamerasWithControls cameras2) { this.LogVerbose("Adding IHasCamerasWithControlsMessenger for {deviceKey}", device.Key @@ -1343,7 +1316,7 @@ namespace PepperDash.Essentials } } - var simplMessengers = _messengers.OfType().ToList(); + var simplMessengers = _messengers.Values.OfType().ToList(); if (simplMessengers.Count > 0) { @@ -1404,8 +1377,9 @@ namespace PepperDash.Essentials _wsClient2.Log.Level = LogLevel.Trace; _wsClient2.SslConfiguration.EnabledSslProtocols = - System.Security.Authentication.SslProtocols.Tls11 - | System.Security.Authentication.SslProtocols.Tls12; + // System.Security.Authentication.SslProtocols.Tls11 + System.Security.Authentication.SslProtocols.Tls12 + | System.Security.Authentication.SslProtocols.Tls13; _wsClient2.OnMessage += HandleMessage; _wsClient2.OnOpen += HandleOpen; @@ -2091,13 +2065,17 @@ namespace PepperDash.Essentials private void ResetPingTimer() { // This tells us we're online with the API and getting pings - _pingTimer.Reset(PingInterval); + _pingTimer.Stop(); + _pingTimer.Interval = PingInterval; + _pingTimer.Start(); } private void StartPingTimer() { StopPingTimer(); - _pingTimer = new CTimer(PingTimerCallback, null, PingInterval); + _pingTimer = new Timer(PingInterval) { AutoReset = false }; + _pingTimer.Elapsed += (s, e) => PingTimerCallback(null); + _pingTimer.Start(); } private void StopPingTimer() @@ -2136,10 +2114,9 @@ namespace PepperDash.Essentials private void StartServerReconnectTimer() { StopServerReconnectTimer(); - _serverReconnectTimer = new CTimer( - ReconnectToServerTimerCallback, - ServerReconnectInterval - ); + _serverReconnectTimer = new Timer(ServerReconnectInterval) { AutoReset = false }; + _serverReconnectTimer.Elapsed += (s, e) => ReconnectToServerTimerCallback(null); + _serverReconnectTimer.Start(); this.LogDebug("Reconnect Timer Started."); } diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs index 85f29983..412507e8 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs @@ -87,7 +87,7 @@ namespace PepperDash.Essentials.Room.MobileControl Eisc = new ThreeSeriesTcpIpEthernetIntersystemCommunications(ipId, "127.0.0.2", Global.ControlSystem); var reg = Eisc.Register(); if (reg != eDeviceRegistrationUnRegistrationResponse.Success) - Debug.Console(0, this, "Cannot connect EISC at IPID {0}: \r{1}", ipId, reg); + this.LogWarning("Cannot connect EISC at IPID {0}: \r{1}", ipId, reg); JoinMap = new MobileControlSIMPLRoomJoinMap(1); @@ -109,7 +109,7 @@ namespace PepperDash.Essentials.Room.MobileControl return; } - Debug.Console(1, this, "SIMPL EISC online={0}. Config is ready={1}. Use Essentials Config={2}", + this.LogDebug("SIMPL EISC online={0}. Config is ready={1}. Use Essentials Config={2}", a.DeviceOnLine, Eisc.BooleanOutput[JoinMap.ConfigIsReady.JoinNumber].BoolValue, Eisc.BooleanOutput[JoinMap.ConfigIsLocal.JoinNumber].BoolValue); @@ -143,7 +143,7 @@ namespace PepperDash.Essentials.Room.MobileControl /// public override bool CustomActivate() { - Debug.Console(0, this, "Final activation. Setting up actions and feedbacks"); + this.LogDebug("Final activation. Setting up actions and feedbacks"); //SetupFunctions(); //SetupFeedbacks(); @@ -169,7 +169,6 @@ namespace PepperDash.Essentials.Room.MobileControl _directRouteMessenger.JoinMap.PrintJoinMapInfo(); - // TODO: Update Source Bridge to use new JoinMap scheme //_sourceBridge.JoinMap.PrintJoinMapInfo(); }, "printmobilebridge", "Prints MC-SIMPL bridge EISC data", ConsoleAccessLevelEnum.AccessOperator); @@ -182,7 +181,7 @@ namespace PepperDash.Essentials.Room.MobileControl SetupDeviceMessengers(); - Debug.Console(0, this, "******* ESSENTIALS CONFIG: \r{0}", + this.LogInformation("******* ESSENTIALS CONFIG: \r{0}", JsonConvert.SerializeObject(ConfigReader.ConfigObject, Formatting.Indented)); ConfigurationIsReady?.Invoke(this, new EventArgs()); @@ -497,7 +496,7 @@ namespace PepperDash.Essentials.Room.MobileControl /// private void LoadConfigValues() { - Debug.Console(1, this, "Loading configuration from SIMPL EISC bridge"); + this.LogInformation("Loading configuration from SIMPL EISC bridge"); ConfigIsLoaded = false; var co = ConfigReader.ConfigObject; @@ -519,12 +518,12 @@ namespace PepperDash.Essentials.Room.MobileControl var rm = new DeviceConfig(); if (co.Rooms.Count == 0) { - Debug.Console(0, this, "Adding room to config"); + this.LogInformation("Adding room to config"); co.Rooms.Add(rm); } else { - Debug.Console(0, this, "Replacing Room[0] in config"); + this.LogInformation("Replacing Room[0] in config"); co.Rooms[0] = rm; } rm.Name = Eisc.StringOutput[JoinMap.ConfigRoomName.JoinNumber].StringValue; @@ -620,7 +619,7 @@ namespace PepperDash.Essentials.Room.MobileControl var controllable = Eisc.BooleanOutput[JoinMap.SourceIsControllableJoinStart.JoinNumber + i].BoolValue; var audioSource = Eisc.BooleanOutput[JoinMap.SourceIsAudioSourceJoinStart.JoinNumber + i].BoolValue; - Debug.Console(0, this, "Adding source {0} '{1}'", key, name); + this.LogInformation("Adding source {0} '{1}'", key, name); var sourceKey = Eisc.StringOutput[JoinMap.SourceControlDeviceKeyJoinStart.JoinNumber + i].StringValue; @@ -645,16 +644,16 @@ namespace PepperDash.Essentials.Room.MobileControl // Look to see if this is a device that already exists in Essentials and get it if (existingSourceDevice != null) { - Debug.Console(0, this, "Found device with key: {0} in Essentials.", key); + this.LogInformation("Found device with key: {0} in Essentials.", key); if (existingSourceDevice.Properties.Value(_syntheticDeviceKey)) { - Debug.Console(0, this, "Updating previous device config with new values"); + this.LogInformation("Updating previous device config with new values"); existingSourceDevice = syntheticDevice; } else { - Debug.Console(0, this, "Using existing Essentials device (non synthetic)"); + this.LogInformation("Using existing Essentials device (non synthetic)"); } } else @@ -759,7 +758,7 @@ namespace PepperDash.Essentials.Room.MobileControl SetupDeviceMessengers(); - Debug.Console(0, this, "******* CONFIG FROM SIMPL: \r{0}", + this.LogDebug("******* CONFIG FROM SIMPL: \r{0}", JsonConvert.SerializeObject(ConfigReader.ConfigObject, Formatting.Indented)); ConfigurationIsReady?.Invoke(this, new EventArgs()); @@ -806,7 +805,7 @@ namespace PepperDash.Essentials.Room.MobileControl continue; } - Debug.Console(0, this, "Adding destination {0} - {1}", key, name); + this.LogInformation("Adding destination {0} - {1}", key, name); eRoutingSignalType parsedType; try @@ -815,7 +814,7 @@ namespace PepperDash.Essentials.Room.MobileControl } catch { - Debug.Console(0, this, "Error parsing destination type: {0}", routeType); + this.LogInformation("Error parsing destination type: {0}", routeType); parsedType = eRoutingSignalType.AudioVideo; } @@ -852,15 +851,15 @@ namespace PepperDash.Essentials.Room.MobileControl if (existingDev != null) { - Debug.Console(0, this, "Found device with key: {0} in Essentials.", key); + this.LogInformation("Found device with key: {0} in Essentials.", key); if (existingDev.Properties.Value(_syntheticDeviceKey)) { - Debug.Console(0, this, "Updating previous device config with new values"); + this.LogInformation("Updating previous device config with new values"); } else { - Debug.Console(0, this, "Using existing Essentials device (non synthetic)"); + this.LogInformation("Using existing Essentials device (non synthetic)"); } } else @@ -901,7 +900,7 @@ namespace PepperDash.Essentials.Room.MobileControl if (DeviceManager.GetDeviceForKey(messengerKey) != null) { - Debug.Console(2, this, "Messenger with key: {0} already exists. Skipping...", messengerKey); + this.LogDebug("Messenger with key: {0} already exists. Skipping...", messengerKey); continue; } @@ -909,7 +908,7 @@ namespace PepperDash.Essentials.Room.MobileControl if (dev == null) { - Debug.Console(1, this, "Unable to find device config for key: '{0}'", props.DeviceKey); + this.LogDebug("Unable to find device config for key: '{0}'", props.DeviceKey); continue; } @@ -918,13 +917,13 @@ namespace PepperDash.Essentials.Room.MobileControl if (type.Equals("simplcameramessenger")) { - Debug.Console(2, this, "Adding SIMPLCameraMessenger for: '{0}'", props.DeviceKey); + this.LogDebug("Adding SIMPLCameraMessenger for: '{0}'", props.DeviceKey); messenger = new SIMPLCameraMessenger(messengerKey, Eisc, "/device/" + props.DeviceKey, props.JoinStart); } else if (type.Equals("simplroutemessenger")) { - Debug.Console(2, this, "Adding SIMPLRouteMessenger for: '{0}'", props.DeviceKey); + this.LogDebug("Adding SIMPLRouteMessenger for: '{0}'", props.DeviceKey); messenger = new SIMPLRouteMessenger(messengerKey, Eisc, "/device/" + props.DeviceKey, props.JoinStart); } @@ -937,7 +936,7 @@ namespace PepperDash.Essentials.Room.MobileControl } else { - Debug.Console(2, this, "Unable to add messenger for device: '{0}' of type: '{1}'", + this.LogDebug("Unable to add messenger for device: '{0}' of type: '{1}'", props.DeviceKey, type); } } @@ -950,7 +949,7 @@ namespace PepperDash.Essentials.Room.MobileControl if (dev is CameraBase) { var camDevice = dev as CameraBase; - Debug.Console(1, this, "Adding CameraBaseMessenger for device: {0}", dev.Key); + this.LogDebug("Adding CameraBaseMessenger for device: {0}", dev.Key); var cameraMessenger = new CameraBaseMessenger(device.Key + "-" + Key, camDevice, "/device/" + device.Key); DeviceMessengers.Add(device.Key, cameraMessenger); @@ -964,7 +963,8 @@ namespace PepperDash.Essentials.Room.MobileControl } catch (Exception e) { - Debug.Console(2, this, "Error Setting up Device Managers: {0}", e); + this.LogException(e,"Error Setting up Device Managers: {0}", e.Message); + this.LogVerbose("Stack Trace:\r{0}", e.StackTrace); } } @@ -977,7 +977,7 @@ namespace PepperDash.Essentials.Room.MobileControl { var count = Eisc.UShortOutput[JoinMap.NumberOfAuxFaders.JoinNumber].UShortValue; - Debug.Console(1, this, "The Fader Count is : {0}", count); + this.LogDebug("The Fader Count is : {0}", count); // build volumes object, serialize and put in content of method below @@ -1013,8 +1013,6 @@ namespace PepperDash.Essentials.Room.MobileControl NumberOfAuxFaders = Eisc.UShortInput[JoinMap.NumberOfAuxFaders.JoinNumber].UShortValue }; - // TODO: Add property to status message to indicate if advanced sharing is supported and if users can change share mode - PostStatus(new { activityMode = GetActivityMode(), @@ -1082,7 +1080,7 @@ namespace PepperDash.Essentials.Room.MobileControl private void EISC_SigChange(object currentDevice, SigEventArgs args) { if (Debug.Level >= 1) - Debug.Console(1, this, "SIMPL EISC change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, + this.LogVerbose("SIMPL EISC change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); var uo = args.Sig.UserObject; if (uo != null) @@ -1122,12 +1120,12 @@ namespace PepperDash.Essentials.Room.MobileControl protected override void UserCodeChange() { - Debug.Console(1, this, "Server user code changed: {0}", UserCode); + this.LogDebug("Server user code changed: {0}", UserCode); var qrUrl = string.Format("{0}/api/rooms/{1}/{3}/qr?x={2}", AppServerController.Host, AppServerController.SystemUuid, new Random().Next(), "room1"); QrCodeUrl = qrUrl; - Debug.Console(1, this, "Server user code changed: {0} - {1}", UserCode, qrUrl); + this.LogDebug("Server user code changed: {0} - {1}", UserCode, qrUrl); OnUserCodeChanged(); diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index f1af6bbf..da357fc5 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -1329,7 +1329,6 @@ namespace PepperDash.Essentials.WebSocketServer var suffix = filePath.Substring(_userAppBaseHref.Length, filePath.Length - _userAppBaseHref.Length); if (suffix != "/") { - //Debug.Console(2, this, "Suffix: {0}", suffix); filePath = filePath.Replace(suffix, ""); } } From 7bec96e68bc1191faed0c6b4c7c4afbbf63514da Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 30 Mar 2026 12:11:21 -0600 Subject: [PATCH 028/161] refactor: Remove obsolete interfaces and classes to streamline the codebase --- .../DeviceTypeInterfaces/IDisplayBasic.cs | 60 ------------- .../Feedbacks/FeedbackBase.cs | 6 -- .../Feedbacks/SerialFeedback.cs | 13 ++- .../Config/EssentialsVolumeLevelConfig.cs | 35 -------- .../Shades/ShadeBase.cs | 48 ---------- .../Displays/InputInterfaces.cs | 87 ------------------- .../Room/IEssentialsHuddleSpaceRoom.cs | 27 ------ .../Room/IEssentialsHuddleVtc1Room.cs | 53 ----------- .../ClientSpecificUpdateRequest.cs | 25 ------ .../MobileControlEssentialsRoomBridge.cs | 38 -------- 10 files changed, 9 insertions(+), 383 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplayBasic.cs delete mode 100644 src/PepperDash.Essentials.Core/Room/Config/EssentialsVolumeLevelConfig.cs delete mode 100644 src/PepperDash.Essentials.Core/Shades/ShadeBase.cs delete mode 100644 src/PepperDash.Essentials.Devices.Common/Displays/InputInterfaces.cs delete mode 100644 src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleSpaceRoom.cs delete mode 100644 src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleVtc1Room.cs delete mode 100644 src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplayBasic.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplayBasic.cs deleted file mode 100644 index e744ac15..00000000 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplayBasic.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -namespace PepperDash.Essentials.Core.Devices.DeviceTypeInterfaces -{ - /// - /// Defines the contract for IDisplayBasic - /// - [Obsolete("This interface is no longer used and will be removed in a future version. Please use IDisplay instead.")] - public interface IDisplayBasic - { - /// - /// Sets the input to HDMI 1 - /// - void InputHdmi1(); - - /// - /// Sets the input to HDMI 2 - /// - void InputHdmi2(); - - /// - /// Sets the input to HDMI 3 - /// - void InputHdmi3(); - - /// - /// Sets the input to HDMI 4 - /// - void InputHdmi4(); - - /// - /// Sets the input to DisplayPort 1 - /// - void InputDisplayPort1(); - - /// - /// Sets the input to DVI 1 - /// - void InputDvi1(); - - /// - /// Sets the input to Video 1 - /// - void InputVideo1(); - - /// - /// Sets the input to VGA 1 - /// - void InputVga1(); - - /// - /// Sets the input to VGA 2 - /// - void InputVga2(); - - /// - /// Sets the input to RGB 1 - /// - void InputRgb1(); - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs index c230bbe8..7163764a 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs @@ -43,12 +43,6 @@ namespace PepperDash.Essentials.Core /// public bool InTestMode { get; protected set; } - /// - /// Base Constructor - empty - /// - [Obsolete("use constructor with Key parameter. This constructor will be removed in a future version")] - protected Feedback() : this(null) { } - /// /// Constructor with Key parameter /// diff --git a/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs index 6fe0be62..0b4ef534 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs @@ -13,7 +13,9 @@ namespace PepperDash.Essentials.Core; /// public class SerialFeedback : Feedback { - public override string SerialValue { get { return _SerialValue; } } + + /// + public override string SerialValue { get { return _SerialValue; } } string _SerialValue; //public override eCueType Type { get { return eCueType.Serial; } } @@ -25,20 +27,20 @@ public class SerialFeedback : Feedback List LinkedInputSigs = new List(); - public SerialFeedback() - { - } + /// public SerialFeedback(string key) : base(key) { } + /// public override void FireUpdate() { throw new NotImplementedException("This feedback type does not use Funcs"); } + /// public void FireUpdate(string newValue) { _SerialValue = newValue; @@ -46,17 +48,20 @@ public class SerialFeedback : Feedback OnOutputChange(newValue); } + /// public void LinkInputSig(StringInputSig sig) { LinkedInputSigs.Add(sig); UpdateSig(sig); } + /// public void UnlinkInputSig(StringInputSig sig) { LinkedInputSigs.Remove(sig); } + /// public override string ToString() { return (InTestMode ? "TEST -- " : "") + SerialValue; diff --git a/src/PepperDash.Essentials.Core/Room/Config/EssentialsVolumeLevelConfig.cs b/src/PepperDash.Essentials.Core/Room/Config/EssentialsVolumeLevelConfig.cs deleted file mode 100644 index c1c34b9c..00000000 --- a/src/PepperDash.Essentials.Core/Room/Config/EssentialsVolumeLevelConfig.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using PepperDash.Essentials.Core; - -namespace PepperDash.Essentials.Room.Config; - -/// -/// Configuration class for volume levels in the Essentials room. This is used to configure the volume levels for the master, program, audio call receive, and audio call transmit channels in the room. -/// -[Obsolete("This class is being deprecated in favor audio control point lists in the main config. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type.")] -public class EssentialsRoomVolumesConfig -{ - public EssentialsVolumeLevelConfig Master { get; set; } - public EssentialsVolumeLevelConfig Program { get; set; } - public EssentialsVolumeLevelConfig AudioCallRx { get; set; } - public EssentialsVolumeLevelConfig AudioCallTx { get; set; } -} - -/// -/// Configuration class for a volume level in the Essentials room. -/// -public class EssentialsVolumeLevelConfig -{ - public string DeviceKey { get; set; } - public string Label { get; set; } - public int Level { get; set; } - - /// - /// Helper to get the device associated with key - one timer. - /// - [Obsolete("This method references DM CHASSIS Directly and should not be used in the Core library. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type.")] - public IBasicVolumeWithFeedback GetDevice() - { - throw new NotImplementedException("This method references DM CHASSIS Directly and should not be used in the Core library. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type."); - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Shades/ShadeBase.cs b/src/PepperDash.Essentials.Core/Shades/ShadeBase.cs deleted file mode 100644 index c0808989..00000000 --- a/src/PepperDash.Essentials.Core/Shades/ShadeBase.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; - -using PepperDash.Core; -using PepperDash.Essentials.Core.CrestronIO; - -namespace PepperDash.Essentials.Core.Shades; - -/// -/// Base class for shades -/// -[Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")] -public abstract class ShadeBase : EssentialsDevice, IShadesOpenCloseStop -{ - /// - /// Constructor - /// - /// key of the shade device - /// name of the shade device - public ShadeBase(string key, string name) - : base(key, name) - { - - } - - #region iShadesOpenClose Members - - /// - /// Opens the shade - /// - public abstract void Open(); - - /// - /// Stops the shade - /// - public abstract void Stop(); - - /// - /// Closes the shade - /// - public abstract void Close(); - - #endregion -} - diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/InputInterfaces.cs b/src/PepperDash.Essentials.Devices.Common/Displays/InputInterfaces.cs deleted file mode 100644 index 4d04fd28..00000000 --- a/src/PepperDash.Essentials.Devices.Common/Displays/InputInterfaces.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; - -namespace PepperDash.Essentials.Devices.Displays; - -/// -/// Defines the contract for IInputHdmi1 -/// -[Obsolete()] -public interface IInputHdmi1 -{ - /// - /// Switches to HDMI 1 input - /// - void InputHdmi1(); -} - -/// -/// Defines the contract for IInputHdmi2 -/// -[Obsolete()] -public interface IInputHdmi2 -{ - /// - /// Switches to HDMI 2 input - /// - void InputHdmi2(); -} - -/// -/// Defines the contract for IInputHdmi3 -/// -[Obsolete()] -public interface IInputHdmi3 -{ - /// - /// Switches to HDMI 3 input - /// - void InputHdmi3(); -} - -/// -/// Defines the contract for IInputHdmi4 -/// -[Obsolete()] -public interface IInputHdmi4 -{ - /// - /// Switches to HDMI 4 input - /// - void InputHdmi4(); -} - -/// -/// Defines the contract for IInputDisplayPort1 -/// -[Obsolete()] -public interface IInputDisplayPort1 -{ - /// - /// Switches to DisplayPort 1 input - /// - void InputDisplayPort1(); -} - -/// -/// Defines the contract for IInputDisplayPort2 -/// -[Obsolete()] -public interface IInputDisplayPort2 -{ - /// - /// Switches to DisplayPort 2 input - /// - void InputDisplayPort2(); -} - -/// -/// Defines the contract for IInputVga1 -/// -[Obsolete()] -public interface IInputVga1 -{ - /// - /// Switches to VGA 1 input - /// - void InputVga1(); -} diff --git a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleSpaceRoom.cs b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleSpaceRoom.cs deleted file mode 100644 index 6a1a816d..00000000 --- a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleSpaceRoom.cs +++ /dev/null @@ -1,27 +0,0 @@ -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Room.Config; - -namespace PepperDash.Essentials.Devices.Common.Room; - -/// -/// Defines the contract for IEssentialsHuddleSpaceRoom -/// -public interface IEssentialsHuddleSpaceRoom : IEssentialsRoom, IHasCurrentSourceInfoChange, IRunRouteAction, IHasDefaultDisplay, IHasCurrentVolumeControls, IRoomOccupancy, - IEmergency, IMicrophonePrivacy -{ - /// - /// Gets whether to exclude this room from global functions - /// - bool ExcludeFromGlobalFunctions { get; } - - /// - /// Runs the route action for the given routeKey and sourceListKey - /// - /// The route key - void RunRouteAction(string routeKey); - - /// - /// Gets the PropertiesConfig - /// - EssentialsHuddleRoomPropertiesConfig PropertiesConfig { get; } -} diff --git a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleVtc1Room.cs b/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleVtc1Room.cs deleted file mode 100644 index 728e1e1b..00000000 --- a/src/PepperDash.Essentials.Devices.Common/Room/IEssentialsHuddleVtc1Room.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Devices.Common.AudioCodec; -using PepperDash.Essentials.Devices.Common.Codec; -using PepperDash.Essentials.Devices.Common.VideoCodec; -using PepperDash.Essentials.Room.Config; - -namespace PepperDash.Essentials.Devices.Common.Room; - - -/// -/// Defines the contract for IEssentialsHuddleVtc1Room -/// -[Obsolete("Obsolete in favor of IEssentialsRoom. This interface will be removed in a future release.")] -public interface IEssentialsHuddleVtc1Room : IEssentialsRoom, IHasCurrentSourceInfoChange, IHasCurrentVolumeControls, IRunRouteAction, IRunDefaultCallRoute, IHasVideoCodec, IHasAudioCodec, IHasDefaultDisplay, IHasInCallFeedback, - IRoomOccupancy, IEmergency, IMicrophonePrivacy -{ - /// - /// Gets the PropertiesConfig - /// - EssentialsHuddleVtc1PropertiesConfig PropertiesConfig { get; } - - /// - /// Gets whether to exclude this room from global functions - /// - bool ExcludeFromGlobalFunctions { get; } - - /// - /// Runs the route action for the given routeKey and sourceListKey - /// - /// The route key - void RunRouteAction(string routeKey); - - /// - /// Gets the ScheduleSource - /// - IHasScheduleAwareness ScheduleSource { get; } - - /// - /// Gets the InCallFeedback - /// - new BoolFeedback InCallFeedback { get; } - - /// - /// Gets the PrivacyModeIsOnFeedback - /// - new BoolFeedback PrivacyModeIsOnFeedback { get; } - - /// - /// Gets the DefaultCodecRouteString - /// - string DefaultCodecRouteString { get; } -} diff --git a/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs b/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs deleted file mode 100644 index b974a9aa..00000000 --- a/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace PepperDash.Essentials -{ - /// - /// Send an update request for a specific client - /// - [Obsolete] - public class ClientSpecificUpdateRequest - { - /// - /// Initialize an instance of the class. - /// - /// - public ClientSpecificUpdateRequest(Action action) - { - ResponseMethod = action; - } - - /// - /// Gets or sets the ResponseMethod - /// - public Action ResponseMethod { get; private set; } - } -} diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs index c486010d..93924b24 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -143,22 +143,6 @@ namespace PepperDash.Essentials.RoomBridges if (Room is IHasCurrentSourceInfoChange sscRoom) sscRoom.CurrentSourceChange += Room_CurrentSingleSourceChange; - if (Room is IEssentialsHuddleVtc1Room vtcRoom) - { - if (vtcRoom.ScheduleSource != null) - { - var key = vtcRoom.Key + "-" + Key; - - if (!AppServerController.CheckForDeviceMessenger(key)) - { - var scheduleMessenger = new IHasScheduleAwarenessMessenger(key, vtcRoom.ScheduleSource, - $"/room/{vtcRoom.Key}"); - AppServerController.AddDeviceMessenger(scheduleMessenger); - } - } - - vtcRoom.InCallFeedback.OutputChange += InCallFeedback_OutputChange; - } if (Room is IPrivacy privacyRoom) { @@ -557,11 +541,6 @@ namespace PepperDash.Essentials.RoomBridges IsCoolingDown = room.IsCoolingDownFeedback.BoolValue }; - if (room is IEssentialsHuddleVtc1Room vtcRoom) - { - state.IsInCall = vtcRoom.InCallFeedback.BoolValue; - } - return state; } catch (Exception ex) @@ -619,23 +598,6 @@ namespace PepperDash.Essentials.RoomBridges configuration.HelpMessage = propertiesConfig.PropertiesConfig.HelpMessageForDisplay; } - if (room is IEssentialsHuddleSpaceRoom huddleRoom && !string.IsNullOrEmpty(huddleRoom.PropertiesConfig.HelpMessageForDisplay)) - { - this.LogVerbose("Getting huddle room config"); - configuration.HelpMessage = huddleRoom.PropertiesConfig.HelpMessageForDisplay; - configuration.UiBehavior = huddleRoom.PropertiesConfig.UiBehavior; - configuration.DefaultPresentationSourceKey = huddleRoom.PropertiesConfig.DefaultSourceItem; - - } - - if (room is IEssentialsHuddleVtc1Room vtc1Room && !string.IsNullOrEmpty(vtc1Room.PropertiesConfig.HelpMessageForDisplay)) - { - this.LogVerbose("Getting vtc room config"); - configuration.HelpMessage = vtc1Room.PropertiesConfig.HelpMessageForDisplay; - configuration.UiBehavior = vtc1Room.PropertiesConfig.UiBehavior; - configuration.DefaultPresentationSourceKey = vtc1Room.PropertiesConfig.DefaultSourceItem; - } - if (room is IEssentialsTechRoom techRoom && !string.IsNullOrEmpty(techRoom.PropertiesConfig.HelpMessage)) { this.LogVerbose("Getting tech room config"); From 5c26438a1c684f0a9afac7daa45664c36c898df7 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 31 Mar 2026 19:29:58 -0600 Subject: [PATCH 029/161] refactor: Remove DebugWebsocketSink.cs to streamline the codebase --- src/PepperDash.Essentials.Core/Web/DebugWebsocketSink.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/Web/DebugWebsocketSink.cs diff --git a/src/PepperDash.Essentials.Core/Web/DebugWebsocketSink.cs b/src/PepperDash.Essentials.Core/Web/DebugWebsocketSink.cs deleted file mode 100644 index e69de29b..00000000 From c98b48ff874cd2a80c5e1d2ad275b48078619733 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 7 Apr 2026 15:53:00 -0600 Subject: [PATCH 030/161] refactor: Clean up and streamline Debug and WebApiServer classes for improved readability and performance --- src/PepperDash.Core/Logging/Debug.cs | 184 +++++++-------- .../Logging/DebugWebsocketSink.cs | 218 +++++++++++++----- src/PepperDash.Core/Web/WebApiServer.cs | 47 ++-- .../Config/Essentials/ConfigReader.cs | 82 +++---- .../Web/EssentialsWebApi.cs | 15 +- .../DebugSessionRequestHandler.cs | 31 ++- src/PepperDash.Essentials/ControlSystem.cs | 82 ++++--- 7 files changed, 379 insertions(+), 280 deletions(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 2dfa7698..cf166c78 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -114,7 +114,7 @@ public static class Debug /// public static string PepperDashCoreVersion { get; private set; } - private static Timer _saveTimer; + // private static Timer _saveTimer; private const int defaultConsoleDebugTimeoutMin = 120; @@ -235,7 +235,7 @@ public static class Debug "appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator); } - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; + // CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; DoNotLoadConfigOnNextBoot = GetDoNotLoadOnNextBoot(); @@ -249,7 +249,8 @@ public static class Debug } catch (Exception ex) { - LogError(ex, "Exception in Debug static constructor: {message}", ex.Message); + // _logger may not have been initialized yet — do not call LogError here. + CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); } } @@ -337,26 +338,26 @@ public static class Debug } } - /// - /// Used to save memory when shutting down - /// - /// - static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { + // /// + // /// Used to save memory when shutting down + // /// + // /// + // static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + // { - if (programEventType == eProgramStatusEventType.Stopping) - { - Log.CloseAndFlush(); + // if (programEventType == eProgramStatusEventType.Stopping) + // { + // Log.CloseAndFlush(); - if (_saveTimer != null) - { - _saveTimer.Stop(); - _saveTimer = null; - } - LogMessage(LogEventLevel.Information, "Saving debug settings"); - SaveMemory(); - } - } + // if (_saveTimer != null) + // { + // _saveTimer.Stop(); + // _saveTimer = null; + // } + // LogMessage(LogEventLevel.Information, "Saving debug settings"); + // // SaveMemory(); + // } + // } /// /// Callback for console command @@ -632,7 +633,7 @@ public static class Debug public static void SetDeviceDebugSettings(string deviceKey, object settings) { _contexts.SetDebugSettingsForKey(deviceKey, settings); - SaveMemoryOnTimeout(); + // SaveMemoryOnTimeout(); } /// @@ -1005,83 +1006,82 @@ public static class Debug } - /// - /// Writes the memory object after timeout - /// - static void SaveMemoryOnTimeout() - { - LogInformation("Saving debug settings"); - if (_saveTimer == null) - { - _saveTimer = new Timer(SaveTimeoutMs) { AutoReset = false }; - _saveTimer.Elapsed += (s, e) => - { - _saveTimer = null; - SaveMemory(); - }; - _saveTimer.Start(); - } - else - { - _saveTimer.Stop(); - _saveTimer.Interval = SaveTimeoutMs; - _saveTimer.Start(); - } - } + // /// + // /// Writes the memory object after timeout + // /// + // static void SaveMemoryOnTimeout() + // { + // LogInformation("Saving debug settings"); + // if (_saveTimer == null) + // { + // _saveTimer = new Timer(SaveTimeoutMs) { AutoReset = false }; + // _saveTimer.Elapsed += (s, e) => + // { + // _saveTimer = null; + // SaveMemory(); + // }; + // _saveTimer.Start(); + // } + // else + // { + // _saveTimer.Stop(); + // _saveTimer.Interval = SaveTimeoutMs; + // _saveTimer.Start(); + // } + // } - /// - /// Writes the memory - use SaveMemoryOnTimeout - /// - static void SaveMemory() - { - //var dir = @"\NVRAM\debug"; - //if (!Directory.Exists(dir)) - // Directory.Create(dir); + // /// + // /// Writes the memory - use SaveMemoryOnTimeout + // /// + // static void SaveMemory() + // { + // //var dir = @"\NVRAM\debug"; + // //if (!Directory.Exists(dir)) + // // Directory.Create(dir); - try - { - var fileName = GetMemoryFileName(); + // try + // { + // var fileName = GetMemoryFileName(); - LogInformation("Loading debug settings file from {fileName}", fileName); + // LogInformation("Loading debug settings file from {fileName}", fileName); - using (var sw = new StreamWriter(fileName)) - { - var json = JsonConvert.SerializeObject(_contexts); - sw.Write(json); - sw.Flush(); - } - } - catch (Exception ex) - { - ErrorLog.Error("Exception saving debug settings: {message}", ex); - CrestronConsole.PrintLine("Exception saving debug settings: {message}", ex.Message); - return; - } - } + // using (var sw = new StreamWriter(fileName)) + // { + // var json = JsonConvert.SerializeObject(_contexts); + // sw.Write(json); + // sw.Flush(); + // } + // } + // catch (Exception ex) + // { + // LogError("Exception saving debug settings: {message}", ex); + // return; + // } + // } - /// - /// - /// - static void LoadMemory() - { - var file = GetMemoryFileName(); - if (File.Exists(file)) - { - using (var sr = new StreamReader(file)) - { - var json = sr.ReadToEnd(); - _contexts = JsonConvert.DeserializeObject(json); + // /// + // /// + // /// + // static void LoadMemory() + // { + // var file = GetMemoryFileName(); + // if (File.Exists(file)) + // { + // using (var sr = new StreamReader(file)) + // { + // var json = sr.ReadToEnd(); + // _contexts = JsonConvert.DeserializeObject(json); - if (_contexts != null) - { - LogMessage(LogEventLevel.Debug, "Debug memory restored from file"); - return; - } - } - } + // if (_contexts != null) + // { + // LogMessage(LogEventLevel.Debug, "Debug memory restored from file"); + // return; + // } + // } + // } - _contexts = new DebugContextCollection(); - } + // _contexts = new DebugContextCollection(); + // } /// /// Helper to get the file path for this app's debug memory diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index b9c74e68..6f1aee18 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -1,8 +1,10 @@ extern alias NewtonsoftJson; using System; -using Crestron.SimplSharp; -using Org.BouncyCastle.Asn1.X509; +using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Crestron.SimplSharp; using Serilog; using Serilog.Configuration; using Serilog.Core; @@ -11,10 +13,8 @@ using Serilog.Formatting; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using Serilog.Formatting.Json; using System.IO; -using System.Security.Authentication; using WebSocketSharp; using WebSocketSharp.Server; -using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; using WebSocketSharp.Net; namespace PepperDash.Core; @@ -29,21 +29,24 @@ namespace PepperDash.Core; public class DebugWebsocketSink : ILogEventSink, IKeyed { private HttpServer _httpsServer; - + private readonly string _path = "/debug/join/"; private const string _certificateName = "selfCres"; private const string _certificatePassword = "cres12345"; + private static string CertPath => + $"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx"; /// /// Gets the port number on which the HTTPS server is currently running. /// - public int Port - { get - { - - if(_httpsServer == null) return 0; + public int Port + { + get + { + + if (_httpsServer == null) return 0; return _httpsServer.Port; - } + } } /// @@ -55,15 +58,17 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed { get { - if (_httpsServer == null) return ""; - return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}"; + if (_httpsServer == null || !_httpsServer.IsListening) return ""; + var service = _httpsServer.WebSocketServices[_path]; + if (service == null) return ""; + return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}"; } } /// /// Gets a value indicating whether the HTTPS server is currently listening for incoming connections. /// - public bool IsRunning { get => _httpsServer?.IsListening ?? false; } + public bool IsRunning { get => _httpsServer?.IsListening ?? false; } /// public string Key => "DebugWebsocketSink"; @@ -83,7 +88,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed _textFormatter = formatProvider ?? new JsonFormatter(); - if (!File.Exists($"\\user\\{_certificateName}.pfx")) + if (!File.Exists(CertPath)) CreateCert(); CrestronEnvironment.ProgramStatusEventHandler += type => @@ -97,29 +102,65 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed private static void CreateCert() { + // NOTE: This method is called from the constructor, which is itself called during Debug's static + // constructor before _logger is assigned. Do NOT call any Debug.Log* methods here — use + // CrestronConsole.PrintLine only, to avoid a NullReferenceException that would poison the Debug type. try - { - var utility = new BouncyCertificate(); - + { var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0); - Debug.LogInformation("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress); + CrestronConsole.PrintLine(string.Format("CreateCert: DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress)); - var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), [string.Format("{0}.{1}", hostName, domainName), ipAddress], [KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth]); + var subjectName = string.Format("CN={0}.{1}", hostName, domainName); + var fqdn = string.Format("{0}.{1}", hostName, domainName); - //Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested + using var rsa = RSA.Create(2048); + + var request = new CertificateRequest( + subjectName, + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + // Subject Key Identifier + request.CertificateExtensions.Add( + new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); + + // Extended Key Usage: server + client auth + request.CertificateExtensions.Add( + new X509EnhancedKeyUsageExtension( + new OidCollection + { + new Oid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth + new Oid("1.3.6.1.5.5.7.3.2") // id-kp-clientAuth + }, + false)); + + // Subject Alternative Names: DNS + IP + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddDnsName(fqdn); + if (System.Net.IPAddress.TryParse(ipAddress, out var ip)) + sanBuilder.AddIpAddress(ip); + request.CertificateExtensions.Add(sanBuilder.Build()); + + var notBefore = DateTimeOffset.UtcNow; + var notAfter = notBefore.AddYears(2); + + using var cert = request.CreateSelfSigned(notBefore, notAfter); var separator = Path.DirectorySeparatorChar; - - utility.CertificatePassword = _certificatePassword; - utility.WriteCertificate(certificate, @$"{separator}user{separator}", _certificateName); + var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName); + + var pfxBytes = cert.Export(X509ContentType.Pfx, _certificatePassword); + File.WriteAllBytes(outputPath, pfxBytes); + + CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath)); } catch (Exception ex) { - Debug.LogError(ex, "WSS CreateCert Failed: {0}", ex.Message); - Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace); + CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed: {0}\r\n{1}", ex.Message, ex.StackTrace)); } } @@ -137,7 +178,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed var sw = new StringWriter(); _textFormatter.Format(logEvent, sw); - _httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString()); + _httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString()); } /// @@ -152,20 +193,39 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed Debug.LogInformation("Starting Websocket Server on port: {0}", port); - Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword); + Start(port, CertPath, _certificatePassword); + } + + private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword) + { + try + { + // EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid + // key-container persistence failures, and avoids the private key export restriction. + return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); + } + catch (Exception ex) + { + // Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output). + // Delete it, regenerate with the BCL path, and retry once. + CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message)); + try { File.Delete(certPath); } catch { } + CreateCert(); + return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); + } } private void Start(int port, string certPath = "", string certPassword = "") { try { - _httpsServer = new HttpServer(port, true); + _httpsServer = new HttpServer(port, true); if (!string.IsNullOrWhiteSpace(certPath)) { Debug.LogInformation("Assigning SSL Configuration"); - _httpsServer.SslConfiguration.ServerCertificate = new X509Certificate2(certPath, certPassword); + _httpsServer.SslConfiguration.ServerCertificate = LoadOrRecreateCert(certPath, certPassword); _httpsServer.SslConfiguration.ClientCertificateRequired = false; _httpsServer.SslConfiguration.CheckCertificateRevocation = false; _httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12; @@ -180,36 +240,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed _httpsServer.AddWebSocketService(_path); Debug.LogInformation("Assigning Log Info"); _httpsServer.Log.Level = LogLevel.Trace; - _httpsServer.Log.Output = (d, s) => - { - uint level; - - switch(d.Level) - { - case WebSocketSharp.LogLevel.Fatal: - level = 3; - break; - case WebSocketSharp.LogLevel.Error: - level = 2; - break; - case WebSocketSharp.LogLevel.Warn: - level = 1; - break; - case WebSocketSharp.LogLevel.Info: - level = 0; - break; - case WebSocketSharp.LogLevel.Debug: - level = 4; - break; - case WebSocketSharp.LogLevel.Trace: - level = 5; - break; - default: - level = 4; - break; - } - Debug.LogInformation("{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s); - }; + _httpsServer.Log.Output = WriteWebSocketInternalLog; Debug.LogInformation("Starting"); _httpsServer.Start(); @@ -219,6 +250,8 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed { Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message); Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace); + // Null out the server so callers can detect failure via IsRunning / Url null guards. + _httpsServer = null; } } @@ -230,10 +263,69 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed public void StopServer() { Debug.LogInformation("Stopping Websocket Server"); - _httpsServer?.Stop(); - _httpsServer = null; + try + { + if (_httpsServer == null || !_httpsServer.IsListening) + { + return; + } + + // Prevent close-sequence internal websocket logs from re-entering the logging pipeline. + _httpsServer.Log.Output = (d, s) => { }; + + var serviceHost = _httpsServer.WebSocketServices[_path]; + + if (serviceHost == null) + { + _httpsServer.Stop(); + _httpsServer = null; + return; + } + + serviceHost.Sessions.Broadcast("Server is stopping"); + + foreach (var session in serviceHost.Sessions.Sessions) + { + if (session?.Context?.WebSocket != null && session.Context.WebSocket.IsAlive) + { + session.Context.WebSocket.Close(1001, "Server is stopping"); + } + } + + _httpsServer.Stop(); + + _httpsServer = null; + + } + catch (Exception ex) + { + Debug.LogError(ex, "WebSocket Failed to stop gracefully {0}", ex.Message); + Debug.LogVerbose("Stack Trace\r\n{0}", ex.StackTrace); + } } + + private static void WriteWebSocketInternalLog(LogData data, string supplemental) + { + try + { + if (data == null) + { + return; + } + + var message = string.IsNullOrWhiteSpace(data.Message) ? "" : data.Message; + var details = string.IsNullOrWhiteSpace(supplemental) ? string.Empty : string.Format(" | details: {0}", supplemental); + + // Use direct console output to avoid recursive log sink calls. + CrestronConsole.PrintLine(string.Format("WS[{0}] {1} | message: {2}{3}", data.Level, data.Date, message, details)); + } + catch + { + // Never throw from websocket log callback. + } + } + } /// @@ -295,7 +387,7 @@ public class DebugClient : WebSocketBehavior { Debug.LogInformation("DebugClient Created"); } - + /// protected override void OnOpen() { diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs index 099817c7..df8e4b58 100644 --- a/src/PepperDash.Core/Web/WebApiServer.cs +++ b/src/PepperDash.Core/Web/WebApiServer.cs @@ -1,14 +1,9 @@ -extern alias NewtonsoftJson; - -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; -using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; -using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; -using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Core.Logging; @@ -76,10 +71,13 @@ public class WebApiServer : IKeyName Name = string.IsNullOrEmpty(name) ? DefaultName : name; BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; + this.LogInformation("Creating Web API Server with Key: {Key}, Name: {Name}, BasePath: {BasePath}", Key, Name, BasePath); + if (_server == null) _server = new HttpCwsServer(BasePath); _server.setProcessName(Key); _server.HttpRequestHandler = new DefaultRequestHandler(); + _server.ReceivedRequestEvent += ReceivedRequestEventHandler; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler; @@ -104,8 +102,12 @@ public class WebApiServer : IKeyName /// void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) { - // Re-enable the server if the link comes back up and the status should be connected - if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered) + if (ethernetEventArgs.EthernetEventType != eEthernetEventType.LinkUp) + { + return; + } + + if (IsRegistered) { this.LogInformation("Ethernet link up. Server is already registered."); return; @@ -116,14 +118,14 @@ public class WebApiServer : IKeyName Start(); } - /// - /// Initialize method - /// - public void Initialize(string key, string basePath) - { - Key = key; - BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; - } + // /// + // /// Initialize method + // /// + // public void Initialize(string key, string basePath) + // { + // Key = key; + // BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; + // } /// /// Adds a route to CWS @@ -214,12 +216,10 @@ public class WebApiServer : IKeyName return; } - IsRegistered = _server.Unregister() == false; + var unregistered = _server.Unregister(); + IsRegistered = !unregistered; - this.LogDebug("Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful"); - - _server.Dispose(); - _server = null; + this.LogDebug("Stopping server, unregistration {0}", unregistered ? "was successful" : "failed"); } catch (Exception ex) { @@ -240,13 +240,12 @@ public class WebApiServer : IKeyName { try { - var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented); - this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j); + var req = args.Context?.Request; + this.LogVerbose("ReceivedRequestEventHandler: {Method} {Path}", req?.HttpMethod, req?.Path); } catch (Exception ex) { this.LogException(ex, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message); - this.LogVerbose("ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace); } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index 071e5a20..362ab9f0 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -123,68 +123,48 @@ namespace PepperDash.Essentials.Core.Config; { Debug.LogMessage(LogEventLevel.Information, "Loading config file: '{0}'", filePath); + var fileContents = fs.ReadToEnd(); + if (localConfigFound) { - ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject(); - - if (localConfigFound) - { - ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject(); - - Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config"); - - return true; - } - else - { - var parsedConfig = JObject.Parse(fs.ReadToEnd()); - - // 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["versions"] != null; - - if (isV2Config) - { - 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; - } - - // Extract SystemUrl and TemplateUrl into final config output - ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject(); - - if (parsedConfig["system_url"] != null) - { - ConfigObject.SystemUrl = parsedConfig["system_url"].Value(); - } - - if (parsedConfig["template_url"] != null) - { - ConfigObject.TemplateUrl = parsedConfig["template_url"].Value(); - } - } - - Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config"); - - return true; + ConfigObject = JObject.Parse(fileContents).ToObject(); + Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config"); + return ConfigObject != null; } else { - var doubleObj = JObject.Parse(fs.ReadToEnd()); - ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject(); + var parsedConfig = JObject.Parse(fileContents); - // Extract SystemUrl and TemplateUrl into final config output + // 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["versions"] != null; - if (doubleObj["system_url"] != 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 ConfigObject != null; } - if (doubleObj["template_url"] != null) + // Extract SystemUrl and TemplateUrl into final config output + ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject(); + + if (ConfigObject == null) { - ConfigObject.TemplateUrl = doubleObj["template_url"].Value(); + Debug.LogMessage(LogEventLevel.Warning, "Config merge produced a null ConfigObject."); + return false; + } + + if (parsedConfig["system_url"] != null) + { + ConfigObject.SystemUrl = parsedConfig["system_url"].Value(); + } + + if (parsedConfig["template_url"] != null) + { + ConfigObject.TemplateUrl = parsedConfig["template_url"].Value(); } } diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index f73b3f2c..237edd62 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; using PepperDash.Core; @@ -26,10 +27,6 @@ public class EssentialsWebApi : EssentialsDevice ? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber) : "/api"; - private const int DebugTrace = 0; - private const int DebugInfo = 1; - private const int DebugVerbose = 2; - /// /// Gets or sets the BasePath /// @@ -230,7 +227,7 @@ public class EssentialsWebApi : EssentialsDevice _server.Start(); - GetPaths(); + PrintPaths(); return; } @@ -240,11 +237,11 @@ public class EssentialsWebApi : EssentialsDevice _server.Start(); - GetPaths(); + PrintPaths(); } /// - /// Print the available pahts + /// Print the available paths /// /// /// http(s)://{ipaddress}/cws/{basePath} @@ -253,7 +250,7 @@ public class EssentialsWebApi : EssentialsDevice /// /// GetPaths method /// - public void GetPaths() + public void PrintPaths() { Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50)); @@ -267,7 +264,7 @@ public class EssentialsWebApi : EssentialsDevice ? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}" : $"https://{currentIp}/cws{BasePath}"; - Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path); + Debug.LogMessage(LogEventLevel.Information, this, "Server: {path:l}", path); var routeCollection = _server.GetRouteCollection(); if (routeCollection == null) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs index 4dbd00a5..c0dee8bb 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs @@ -6,18 +6,23 @@ using PepperDash.Core.Web.RequestHandlers; using Serilog.Events; using System; using System.Text; +using System.Threading.Tasks; namespace PepperDash.Essentials.Core.Web.RequestHandlers; +/// +/// Represents a DebugSessionRequestHandler +/// public class DebugSessionRequestHandler : WebApiBaseRequestHandler -{ - private readonly DebugWebsocketSink _sink = new DebugWebsocketSink(); +{ + /// + /// Constructor + /// public DebugSessionRequestHandler() : base(true) { } - /// /// Gets details for a debug session /// @@ -41,21 +46,30 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler var port = 0; - if (!_sink.IsRunning) + if (!Debug.WebsocketSink.IsRunning) { Debug.LogMessage(LogEventLevel.Information, "Starting WS Server"); // Generate a random port within a specified range port = new Random().Next(65435, 65535); // Start the WS Server - _sink.StartServerAndSetPort(port); + Debug.WebsocketSink.StartServerAndSetPort(port); Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); } - var url = _sink.Url; + if (!Debug.WebsocketSink.IsRunning) + { + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.Write(JsonConvert.SerializeObject(new { error = "Failed to start WebSocket debug server. Check logs for details." }), false); + context.Response.End(); + return; + } + + var url = Debug.WebsocketSink.Url; object data = new { - url = _sink.Url + url = Debug.WebsocketSink.Url }; Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url); @@ -82,7 +96,8 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler /// protected override void HandlePost(HttpCwsContext context) { - _sink.StopServer(); + + Task.Run(() => Debug.WebsocketSink.StopServer()); context.Response.StatusCode = 200; context.Response.StatusDescription = "OK"; diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index fcb48fdd..63f4160e 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -15,6 +15,7 @@ using System.Threading; using Timeout = Crestron.SimplSharp.Timeout; using Serilog.Events; using System.Threading.Tasks; +using PepperDash.Essentials.Core.Web; namespace PepperDash.Essentials; @@ -56,7 +57,15 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig } catch (Exception e) { - Debug.LogError(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); + try + { + Debug.LogError(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); + } + catch + { + // Debug may not be initialized (e.g. its own static ctor failed); fall back to console. + CrestronConsole.PrintLine($"FATAL INITIALIZE ERROR. System is in an inconsistent state\r\n{e.Message}\r\n{e.StackTrace}"); + } } } @@ -152,6 +161,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); + DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); if (!Debug.DoNotLoadConfigOnNextBoot) { @@ -260,10 +270,10 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig PluginLoader.LoadPlugins(); Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config..."); - if (!ConfigReader.LoadConfig2()) + if (!ConfigReader.LoadConfig2() || ConfigReader.ConfigObject == null) { - Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors"); - return; + Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file. Please ensure a valid config file is present and restart the program."); + // return; } Load(); @@ -385,43 +395,49 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig new Core.Monitoring.SystemMonitorController("systemMonitor")); } - foreach (var devConf in ConfigReader.ConfigObject.Devices) + if (ConfigReader.ConfigObject is not null) { - IKeyed newDev = null; + Debug.LogMessage(LogEventLevel.Warning, "LoadDevices: ConfigObject is null. Cannot load devices."); - try + foreach (var devConf in ConfigReader.ConfigObject.Devices) { - Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type); - // Skip this to prevent unnecessary warnings - if (devConf.Key == "processor") + IKeyed newDev = null; + + try { - var prompt = Global.ControlSystem.ControllerPrompt; + Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type); + // Skip this to prevent unnecessary warnings + if (devConf.Key == "processor") + { + var prompt = Global.ControlSystem.ControllerPrompt; - var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) || - string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase); + var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) || + string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase); - if (!typeMatch) - Debug.LogMessage(LogEventLevel.Information, - "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", - devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); + if (!typeMatch) + Debug.LogMessage(LogEventLevel.Information, + "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", + devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); - continue; + continue; + } + + + if (newDev == null) + newDev = Core.DeviceFactory.GetDevice(devConf); + + if (newDev != null) + DeviceManager.AddDevice(newDev); + else + Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); + } + catch (Exception e) + { + Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); } - - - if (newDev == null) - newDev = Core.DeviceFactory.GetDevice(devConf); - - if (newDev != null) - DeviceManager.AddDevice(newDev); - else - Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); - } - catch (Exception e) - { - Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); } + } Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded."); @@ -438,7 +454,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig var tlc = TieLineCollection.Default; - if (ConfigReader.ConfigObject.TieLines == null) + if (ConfigReader.ConfigObject?.TieLines == null) { return; } @@ -459,7 +475,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig /// public void LoadRooms() { - if (ConfigReader.ConfigObject.Rooms == null) + if (ConfigReader.ConfigObject?.Rooms == null) { Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration."); return; From bfb9838743b3c031787aadbb8cf882339bee90b7 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 11:51:10 -0600 Subject: [PATCH 031/161] refactor: Rename logging level switch variables for clarity and add fallback handler to WebApiServer to allow for serving debug app --- src/PepperDash.Core/Logging/Debug.cs | 19 +- src/PepperDash.Core/Web/WebApiServer.cs | 16 ++ .../Web/EssentialsWebApi.cs | 12 + .../ServeDebugAppRequestHandler.cs | 229 ++++++++++++++++++ 4 files changed, 267 insertions(+), 9 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/ServeDebugAppRequestHandler.cs diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index cf166c78..2e4b25e4 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -51,7 +51,7 @@ public static class Debug private static readonly LoggingLevelSwitch errorLogLevelSwitch; - private static readonly LoggingLevelSwitch fileLevelSwitch; + private static readonly LoggingLevelSwitch fileLoggingLevelSwitch; /// /// The minimum log level for messages to be sent to the console sink @@ -168,7 +168,7 @@ public static class Debug errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel); - fileLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); + fileLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); @@ -193,7 +193,7 @@ public static class Debug rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug, retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, - levelSwitch: fileLevelSwitch + levelSwitch: fileLoggingLevelSwitch ); // Instantiate the root logger @@ -413,7 +413,8 @@ public static class Debug return; } - if (Enum.TryParse(levelString, out var levelEnum)) + // make this parse attempt case-insensitive to allow for more flexible command usage + if (Enum.TryParse(levelString, true, out var levelEnum)) { SetDebugLevel(levelEnum); return; @@ -483,7 +484,7 @@ public static class Debug var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint)level); if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {erro}", err); + LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {error}", err); LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", websocketLoggingLevelSwitch.MinimumLevel); } @@ -502,7 +503,7 @@ public static class Debug if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting: {error}", err); - LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", websocketLoggingLevelSwitch.MinimumLevel); + LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", errorLogLevelSwitch.MinimumLevel); } /// @@ -510,14 +511,14 @@ public static class Debug /// public static void SetFileMinimumDebugLevel(LogEventLevel level) { - errorLogLevelSwitch.MinimumLevel = level; + fileLoggingLevelSwitch.MinimumLevel = level; - var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); + var err = CrestronDataStoreStatic.SetLocalUintValue(FileLevelStoreKey, (uint)level); if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) LogMessage(LogEventLevel.Information, "Error saving File debug level setting: {error}", err); - LogMessage(LogEventLevel.Information, "File debug level set to {0}", websocketLoggingLevelSwitch.MinimumLevel); + LogMessage(LogEventLevel.Information, "File debug level set to {0}", fileLoggingLevelSwitch.MinimumLevel); } /// diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs index df8e4b58..bccd52aa 100644 --- a/src/PepperDash.Core/Web/WebApiServer.cs +++ b/src/PepperDash.Core/Web/WebApiServer.cs @@ -160,6 +160,22 @@ public class WebApiServer : IKeyName _server.Routes.Remove(route); } + /// + /// Sets the fallback request handler that is invoked when no registered route + /// matches an incoming request. Must be called before . + /// + /// The handler to use as the server-level fallback. + public void SetFallbackHandler(IHttpCwsHandler handler) + { + if (handler == null) + { + this.LogWarning("SetFallbackHandler: handler parameter is null, ignoring"); + return; + } + + _server.HttpRequestHandler = handler; + } + /// /// GetRouteCollection method /// diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index 237edd62..8fbc6c34 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -18,6 +18,7 @@ namespace PepperDash.Essentials.Core.Web; public class EssentialsWebApi : EssentialsDevice { private readonly WebApiServer _server; + private readonly WebApiServer _debugServer; /// /// http(s)://{ipaddress}/cws/{basePath} @@ -68,6 +69,9 @@ public class EssentialsWebApi : EssentialsDevice _server = new WebApiServer(Key, Name, BasePath); + _debugServer = new WebApiServer($"{key}-debug-app", $"{name} Debug App", "/debug"); + _debugServer.SetFallbackHandler(new ServeDebugAppRequestHandler()); + SetupRoutes(); } @@ -226,6 +230,7 @@ public class EssentialsWebApi : EssentialsDevice Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series"); _server.Start(); + _debugServer.Start(); PrintPaths(); @@ -236,6 +241,7 @@ public class EssentialsWebApi : EssentialsDevice Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server"); _server.Start(); + _debugServer.Start(); PrintPaths(); } @@ -277,6 +283,12 @@ public class EssentialsWebApi : EssentialsDevice { Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url); } + + var debugPath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server + ? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws/debug" + : $"https://{currentIp}/cws/debug"; + Debug.LogMessage(LogEventLevel.Information, this, "Debug App: {debugPath:l}", debugPath); + Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50)); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/ServeDebugAppRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/ServeDebugAppRequestHandler.cs new file mode 100644 index 00000000..85b17bd6 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/ServeDebugAppRequestHandler.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharp.WebScripting; +using PepperDash.Core; +using PepperDash.Core.Web.RequestHandlers; +using Serilog.Events; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Serves the React debug app from the processor's /HTML/debug folder. +/// The root route (debug) and all sub-paths (debug/{*filePath}) are handled here. +/// Text assets are sent as UTF-8 strings; binary assets are written to the response +/// OutputStream. Any sub-path that does not match a real file falls back to +/// index.html so that client-side (React Router) routing continues to work. +/// +public class ServeDebugAppRequestHandler : WebApiBaseRequestHandler +{ + private static readonly Dictionary MimeTypes = new(StringComparer.OrdinalIgnoreCase) + { + { ".html", "text/html; charset=utf-8" }, + { ".htm", "text/html; charset=utf-8" }, + { ".js", "application/javascript" }, + { ".mjs", "application/javascript" }, + { ".jsx", "application/javascript" }, + { ".css", "text/css" }, + { ".json", "application/json" }, + { ".map", "application/json" }, + { ".svg", "image/svg+xml" }, + { ".ico", "image/x-icon" }, + { ".png", "image/png" }, + { ".jpg", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".gif", "image/gif" }, + { ".woff", "font/woff" }, + { ".woff2","font/woff2" }, + { ".ttf", "font/ttf" }, + { ".eot", "application/vnd.ms-fontobject" }, + }; + + private static readonly HashSet TextExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".html", ".htm", ".js", ".mjs", ".jsx", ".css", ".json", ".map", ".svg", ".txt", ".xml" + }; + + /// + /// Constructor. CORS is enabled so browser dev-tools requests succeed. + /// + public ServeDebugAppRequestHandler() : base(true) { } + + /// + /// Handles GET requests for the debug app and its static assets. + /// + protected override void HandleGet(HttpCwsContext context) + { + // When acting as the server-level fallback handler, only handle + // requests that are actually for the /debug path; defer everything + // else to the base class (which returns 501 Not Implemented). + var rawUrl = context.Request.RawUrl ?? string.Empty; + if (rawUrl.IndexOf("/debug", StringComparison.OrdinalIgnoreCase) < 0) + { + base.HandleGet(context); + return; + } + + try + { + var htmlDebugPath = GetHtmlDebugPath(); + if (htmlDebugPath == null) + { + SendResponse(context, 500, "Internal Server Error"); + return; + } + + var requestedPath = GetRequestedFilePath(context); + + // Paths with no file extension are SPA client-side routes — serve index.html + string candidate; + if (string.IsNullOrEmpty(requestedPath) || !System.IO.Path.HasExtension(requestedPath)) + { + candidate = System.IO.Path.Combine(htmlDebugPath, "index.html"); + } + else + { + var relativePart = requestedPath.Replace('/', System.IO.Path.DirectorySeparatorChar); + candidate = System.IO.Path.Combine(htmlDebugPath, relativePart); + } + + // Resolve to an absolute path and guard against path-traversal attacks + var resolvedCandidate = System.IO.Path.GetFullPath(candidate); + var resolvedBase = System.IO.Path.GetFullPath(htmlDebugPath) + + System.IO.Path.DirectorySeparatorChar; + + if (!resolvedCandidate.StartsWith(resolvedBase, StringComparison.OrdinalIgnoreCase)) + { + SendResponse(context, 403, "Forbidden"); + return; + } + + // Missing static asset → fall back to index.html (SPA deep-link support) + if (!File.Exists(resolvedCandidate) && System.IO.Path.HasExtension(requestedPath ?? string.Empty)) + { + resolvedCandidate = System.IO.Path.Combine(htmlDebugPath, "index.html"); + Debug.LogMessage(LogEventLevel.Debug, + "ServeDebugAppRequestHandler: '{requestedPath:l}' not found, falling back to index.html", + requestedPath); + } + + if (!File.Exists(resolvedCandidate)) + { + SendResponse(context, 404, "Not Found"); + return; + } + + var ext = System.IO.Path.GetExtension(resolvedCandidate); + var contentType = MimeTypes.TryGetValue(ext, out var mime) ? mime : "application/octet-stream"; + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = contentType; + + if (TextExtensions.Contains(ext)) + { + string content; + using (var reader = new StreamReader(resolvedCandidate)) + content = reader.ReadToEnd(); + + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(content, false); + } + else + { + var bytes = System.IO.File.ReadAllBytes(resolvedCandidate); + context.Response.OutputStream.Write(bytes, 0, bytes.Length); + } + + context.Response.End(); + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Error, + "ServeDebugAppRequestHandler: Unhandled error serving '{rawUrl:l}': {ex}", + context.Request.RawUrl, ex.Message); + try { SendResponse(context, 500, "Internal Server Error"); } catch { /* best-effort */ } + } + } + + /// + /// Resolves the absolute path of the /HTML/debug folder on the processor. + /// + /// + /// On a 4-series appliance, Global.FilePathPrefix is + /// {root}/user/programX/, so walking up two parents gives the + /// processor root that contains the html folder. + /// On Virtual Control, Global.FilePathPrefix is {root}/User/, + /// so only one parent hop is needed. + /// + private static string GetHtmlDebugPath() + { + try + { + var separators = new[] { System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar }; + var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(separators)); + + DirectoryInfo rootDir; + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) + { + // Virtual Control: {root}/User/ → one parent up = {root} + rootDir = programDir.Parent; + } + else + { + // 4-series appliance: {root}/user/programX/ → two parents up = {root} + rootDir = programDir.Parent?.Parent; + } + + if (rootDir == null) + { + Debug.LogMessage(LogEventLevel.Error, + "ServeDebugAppRequestHandler: Cannot resolve HTML root from FilePathPrefix '{prefix:l}'", + Global.FilePathPrefix); + return null; + } + + return System.IO.Path.Combine(rootDir.FullName, "html", "debug"); + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Error, + "ServeDebugAppRequestHandler: Error resolving HTML debug path: {ex}", ex.Message); + return null; + } + } + + /// + /// Extracts the file sub-path from the request by parsing RawUrl. + /// Returns an empty string when the URL ends at /debug (root hit). + /// + private static string GetRequestedFilePath(HttpCwsContext context) + { + var rawUrl = context.Request.RawUrl ?? string.Empty; + + // Locate the /debug segment in the URL + const string debugToken = "/debug"; + var idx = rawUrl.IndexOf(debugToken, StringComparison.OrdinalIgnoreCase); + if (idx < 0) + return string.Empty; + + var afterDebug = rawUrl.Substring(idx + debugToken.Length); + + // Strip query string + var qIdx = afterDebug.IndexOf('?'); + if (qIdx >= 0) + afterDebug = afterDebug.Substring(0, qIdx); + + // Strip leading slash to get a relative file path + return afterDebug.TrimStart('/'); + } + + private static void SendResponse(HttpCwsContext context, int statusCode, string statusDescription) + { + context.Response.StatusCode = statusCode; + context.Response.StatusDescription = statusDescription; + context.Response.End(); + } +} From 24df4e7a033c665de7043750328bc373d21c7004 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 12:41:15 -0600 Subject: [PATCH 032/161] feat: Add unit tests and fakes for Crestron environment and data store - Introduced `FakeCrestronEnvironment` and `FakeEthernetHelper` for testing purposes. - Implemented `InMemoryCrestronDataStore` to facilitate unit tests for data storage. - Created `DebugServiceTests` to validate the behavior of debug-related services. - Added project files for unit tests targeting .NET 9.0 with necessary dependencies. - Developed production adapters (`CrestronConsoleAdapter`, `CrestronDataStoreAdapter`, `CrestronEnvironmentAdapter`, `CrestronEthernetAdapter`) to interface with the Crestron SDK. - Updated `Debug` class to utilize injected service abstractions instead of direct SDK calls. - Enhanced `ControlSystem` initialization to register service adapters before usage. --- .../DebugServiceRegistration.cs | 36 ++++ src/PepperDash.Core.Abstractions/Enums.cs | 100 +++++++++ .../ICrestronConsole.cs | 32 +++ .../ICrestronDataStore.cs | 31 +++ .../ICrestronEnvironment.cs | 45 ++++ .../IEthernetHelper.cs | 16 ++ .../PepperDash.Core.Abstractions.csproj | 19 ++ .../Fakes/CrestronConsoleFakes.cs | 38 ++++ .../Fakes/CrestronEnvironmentFakes.cs | 46 ++++ .../Fakes/InMemoryCrestronDataStore.cs | 59 ++++++ .../Logging/DebugServiceTests.cs | 171 +++++++++++++++ .../PepperDash.Core.Tests.csproj | 25 +++ .../Adapters/CrestronConsoleAdapter.cs | 35 ++++ .../Adapters/CrestronDataStoreAdapter.cs | 42 ++++ .../Adapters/CrestronEnvironmentAdapter.cs | 68 ++++++ .../Adapters/CrestronEthernetAdapter.cs | 39 ++++ src/PepperDash.Core/Logging/Debug.cs | 198 ++++++++++-------- src/PepperDash.Core/PepperDash.Core.csproj | 3 + .../Config/ConfigServiceFakesTests.cs | 73 +++++++ .../PepperDash.Essentials.Core.Tests.csproj | 27 +++ src/PepperDash.Essentials/ControlSystem.cs | 10 + 21 files changed, 1020 insertions(+), 93 deletions(-) create mode 100644 src/PepperDash.Core.Abstractions/DebugServiceRegistration.cs create mode 100644 src/PepperDash.Core.Abstractions/Enums.cs create mode 100644 src/PepperDash.Core.Abstractions/ICrestronConsole.cs create mode 100644 src/PepperDash.Core.Abstractions/ICrestronDataStore.cs create mode 100644 src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs create mode 100644 src/PepperDash.Core.Abstractions/IEthernetHelper.cs create mode 100644 src/PepperDash.Core.Abstractions/PepperDash.Core.Abstractions.csproj create mode 100644 src/PepperDash.Core.Tests/Fakes/CrestronConsoleFakes.cs create mode 100644 src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs create mode 100644 src/PepperDash.Core.Tests/Fakes/InMemoryCrestronDataStore.cs create mode 100644 src/PepperDash.Core.Tests/Logging/DebugServiceTests.cs create mode 100644 src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj create mode 100644 src/PepperDash.Core/Adapters/CrestronConsoleAdapter.cs create mode 100644 src/PepperDash.Core/Adapters/CrestronDataStoreAdapter.cs create mode 100644 src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs create mode 100644 src/PepperDash.Core/Adapters/CrestronEthernetAdapter.cs create mode 100644 src/PepperDash.Essentials.Core.Tests/Config/ConfigServiceFakesTests.cs create mode 100644 src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj diff --git a/src/PepperDash.Core.Abstractions/DebugServiceRegistration.cs b/src/PepperDash.Core.Abstractions/DebugServiceRegistration.cs new file mode 100644 index 00000000..c5e18c5a --- /dev/null +++ b/src/PepperDash.Core.Abstractions/DebugServiceRegistration.cs @@ -0,0 +1,36 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Allows pre-registration of Crestron service implementations before the Debug +/// static class initialises. Call from the composition root +/// (e.g. ControlSystem constructor) before any code touches Debug.*. +/// Test projects should call it with no-op / in-memory implementations so that the +/// Debug static constructor never tries to reach the real Crestron SDK. +/// +public static class DebugServiceRegistration +{ + /// Gets the registered environment abstraction, or null if not registered. + public static ICrestronEnvironment? Environment { get; private set; } + + /// Gets the registered console abstraction, or null if not registered. + public static ICrestronConsole? Console { get; private set; } + + /// Gets the registered data-store abstraction, or null if not registered. + public static ICrestronDataStore? DataStore { get; private set; } + + /// + /// Registers the service implementations that Debug will use when its + /// static constructor runs. Any parameter may be null to leave the + /// corresponding service unregistered (the Debug class will skip that + /// capability gracefully). + /// + public static void Register( + ICrestronEnvironment? environment, + ICrestronConsole? console, + ICrestronDataStore? dataStore) + { + Environment = environment; + Console = console; + DataStore = dataStore; + } +} diff --git a/src/PepperDash.Core.Abstractions/Enums.cs b/src/PepperDash.Core.Abstractions/Enums.cs new file mode 100644 index 00000000..cd6052e6 --- /dev/null +++ b/src/PepperDash.Core.Abstractions/Enums.cs @@ -0,0 +1,100 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Mirrors Crestron's eDevicePlatform without requiring the Crestron SDK. +/// +public enum DevicePlatform +{ + /// Hardware appliance (e.g. CP4, MC4). + Appliance, + /// Crestron Virtual Control / server runtime. + Server, +} + +/// +/// Mirrors Crestron's eRuntimeEnvironment. +/// +public enum RuntimeEnvironment +{ + /// SimplSharpPro program slot (hardware 4-series). + SimplSharpPro, + /// SimplSharp (older 3-series or server environments). + SimplSharp, + /// Any other environment — check for completeness. + Other, +} + +/// +/// Mirrors Crestron's ConsoleAccessLevelEnum. +/// +public enum ConsoleAccessLevel +{ + AccessAdministrator = 0, + AccessOperator = 1, + AccessProgrammer = 2, +} + +/// +/// Mirrors Crestron's eProgramStatusEventType. +/// +public enum ProgramStatusEventType +{ + Starting, + Stopping, + Paused, + Resumed, +} + +/// +/// Mirrors the event type used by Crestron's EthernetEventArgs. +/// +public enum EthernetEventType +{ + LinkDown = 0, + LinkUp = 1, +} + +/// +/// Event args for Crestron ethernet link events. +/// +public class PepperDashEthernetEventArgs : EventArgs +{ + public EthernetEventType EthernetEventType { get; } + public short EthernetAdapter { get; } + + public PepperDashEthernetEventArgs(EthernetEventType eventType, short adapter) + { + EthernetEventType = eventType; + EthernetAdapter = adapter; + } +} + +/// +/// Mirrors the set of CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET values +/// used across this codebase — does not aim to be exhaustive. +/// +public enum EthernetParameterType +{ + GetCurrentIpAddress, + GetHostname, + GetDomainName, + GetLinkStatus, + GetCurrentDhcpState, + GetCurrentIpMask, + GetCurrentRouter, + GetMacAddress, + GetDnsServer, +} + +/// +/// Mirrors Crestron's SocketStatus without requiring the Crestron SDK. +/// +public enum PepperDashSocketStatus +{ + SocketNotConnected = 0, + SocketConnected = 2, + SocketConnectionInProgress = 6, + SocketConnectFailed = 11, + SocketDisconnecting = 12, + SocketBrokenRemotely = 7, +} diff --git a/src/PepperDash.Core.Abstractions/ICrestronConsole.cs b/src/PepperDash.Core.Abstractions/ICrestronConsole.cs new file mode 100644 index 00000000..335c4ee0 --- /dev/null +++ b/src/PepperDash.Core.Abstractions/ICrestronConsole.cs @@ -0,0 +1,32 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Abstracts Crestron.SimplSharp.CrestronConsole to allow unit testing +/// without the Crestron SDK. +/// +public interface ICrestronConsole +{ + /// Prints a line to the Crestron console/telnet output. + void PrintLine(string message); + + /// Prints text (without newline) to the Crestron console/telnet output. + void Print(string message); + + /// + /// Sends a response string to the console for the currently-executing console command. + /// + void ConsoleCommandResponse(string message); + + /// + /// Registers a new command with the Crestron console. + /// + /// Handler invoked when the command is typed. + /// Command name (no spaces). + /// Help text shown by the Crestron console. + /// Minimum access level required to run the command. + void AddNewConsoleCommand( + Action callback, + string command, + string helpText, + ConsoleAccessLevel accessLevel); +} diff --git a/src/PepperDash.Core.Abstractions/ICrestronDataStore.cs b/src/PepperDash.Core.Abstractions/ICrestronDataStore.cs new file mode 100644 index 00000000..96fd1856 --- /dev/null +++ b/src/PepperDash.Core.Abstractions/ICrestronDataStore.cs @@ -0,0 +1,31 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Abstracts Crestron.SimplSharp.CrestronDataStore.CrestronDataStoreStatic +/// to allow unit testing without the Crestron SDK. +/// +public interface ICrestronDataStore +{ + /// Initialises the data store. Must be called once before any other operation. + void InitStore(); + + /// Reads an integer value from the local (program-slot) store. + /// true if the value was found and read successfully. + bool TryGetLocalInt(string key, out int value); + + /// Writes an integer value to the local (program-slot) store. + /// true on success. + bool SetLocalInt(string key, int value); + + /// Writes an unsigned integer value to the local (program-slot) store. + /// true on success. + bool SetLocalUint(string key, uint value); + + /// Reads a boolean value from the local (program-slot) store. + /// true if the value was found and read successfully. + bool TryGetLocalBool(string key, out bool value); + + /// Writes a boolean value to the local (program-slot) store. + /// true on success. + bool SetLocalBool(string key, bool value); +} diff --git a/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs b/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs new file mode 100644 index 00000000..c31fb3f6 --- /dev/null +++ b/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs @@ -0,0 +1,45 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Abstracts Crestron.SimplSharp.CrestronEnvironment to allow unit testing +/// without the Crestron SDK. +/// +public interface ICrestronEnvironment +{ + /// Gets the platform the program is executing on. + DevicePlatform DevicePlatform { get; } + + /// Gets the current runtime environment. + RuntimeEnvironment RuntimeEnvironment { get; } + + /// Gets the platform-appropriate newline string. + string NewLine { get; } + + /// Gets the application number (program slot). + uint ApplicationNumber { get; } + + /// Gets the room ID (used in Crestron Virtual Control / server environments). + uint RoomId { get; } + + /// Raised when program status changes (starting, stopping, etc.). + event EventHandler ProgramStatusChanged; + + /// Raised when the ethernet link changes state. + event EventHandler EthernetEventReceived; + + /// Gets the application root directory path. + string GetApplicationRootDirectory(); +} + +/// +/// Event args for . +/// +public class ProgramStatusEventArgs : EventArgs +{ + public ProgramStatusEventType EventType { get; } + + public ProgramStatusEventArgs(ProgramStatusEventType eventType) + { + EventType = eventType; + } +} diff --git a/src/PepperDash.Core.Abstractions/IEthernetHelper.cs b/src/PepperDash.Core.Abstractions/IEthernetHelper.cs new file mode 100644 index 00000000..0ae1c83b --- /dev/null +++ b/src/PepperDash.Core.Abstractions/IEthernetHelper.cs @@ -0,0 +1,16 @@ +namespace PepperDash.Core.Abstractions; + +/// +/// Abstracts Crestron.SimplSharp.CrestronEthernetHelper to allow unit testing +/// without the Crestron SDK. +/// +public interface IEthernetHelper +{ + /// + /// Returns a network parameter string for the specified adapter. + /// + /// The parameter to retrieve. + /// Ethernet adapter index (0 = LAN A). + /// String value of the requested parameter. + string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId); +} diff --git a/src/PepperDash.Core.Abstractions/PepperDash.Core.Abstractions.csproj b/src/PepperDash.Core.Abstractions/PepperDash.Core.Abstractions.csproj new file mode 100644 index 00000000..c1cdf5bc --- /dev/null +++ b/src/PepperDash.Core.Abstractions/PepperDash.Core.Abstractions.csproj @@ -0,0 +1,19 @@ + + + PepperDash.Core.Abstractions + PepperDash.Core.Abstractions + net8.0 + enable + true + en + PepperDash Core Abstractions + PepperDash Technologies + git + https://github.com/PepperDash/PepperDashCore + enable + false + $(Version) + true + + + diff --git a/src/PepperDash.Core.Tests/Fakes/CrestronConsoleFakes.cs b/src/PepperDash.Core.Tests/Fakes/CrestronConsoleFakes.cs new file mode 100644 index 00000000..afd5c324 --- /dev/null +++ b/src/PepperDash.Core.Tests/Fakes/CrestronConsoleFakes.cs @@ -0,0 +1,38 @@ +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Tests.Fakes; + +/// +/// No-op ICrestronConsole that captures output for test assertions. +/// +public class CapturingCrestronConsole : ICrestronConsole +{ + public List Lines { get; } = new(); + public List CommandResponses { get; } = new(); + public List<(string Command, string HelpText)> RegisteredCommands { get; } = new(); + + public void PrintLine(string message) => Lines.Add(message); + public void Print(string message) => Lines.Add(message); + public void ConsoleCommandResponse(string message) => CommandResponses.Add(message); + + public void AddNewConsoleCommand( + Action callback, + string command, + string helpText, + ConsoleAccessLevel accessLevel) + { + RegisteredCommands.Add((command, helpText)); + } +} + +/// +/// Minimal no-op ICrestronConsole that discards all output. Useful when you only +/// care about the system under test and not what it logs. +/// +public class NoOpCrestronConsole : ICrestronConsole +{ + public void PrintLine(string message) { } + public void Print(string message) { } + public void ConsoleCommandResponse(string message) { } + public void AddNewConsoleCommand(Action _, string __, string ___, ConsoleAccessLevel ____) { } +} diff --git a/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs b/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs new file mode 100644 index 00000000..8070f849 --- /dev/null +++ b/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs @@ -0,0 +1,46 @@ +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Tests.Fakes; + +/// +/// Configurable ICrestronEnvironment for unit tests. +/// Defaults: Appliance / SimplSharpPro / ApplicationNumber=1. +/// +public class FakeCrestronEnvironment : ICrestronEnvironment +{ + public DevicePlatform DevicePlatform { get; set; } = DevicePlatform.Appliance; + public RuntimeEnvironment RuntimeEnvironment { get; set; } = RuntimeEnvironment.SimplSharpPro; + public string NewLine { get; set; } = "\r\n"; + public uint ApplicationNumber { get; set; } = 1; + public uint RoomId { get; set; } = 0; + + public event EventHandler? ProgramStatusChanged; + public event EventHandler? EthernetEventReceived; + + public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath(); + + /// Simulates a program status event for tests. + public void RaiseProgramStatus(ProgramStatusEventType type) => + ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type)); + + /// Simulates an ethernet event for tests. + public void RaiseEthernetEvent(EthernetEventType type, short adapter = 0) => + EthernetEventReceived?.Invoke(this, new PepperDashEthernetEventArgs(type, adapter)); +} + +/// +/// No-op IEthernetHelper that returns configurable values. +/// +public class FakeEthernetHelper : IEthernetHelper +{ + private readonly Dictionary _values = new(); + + public FakeEthernetHelper Seed(EthernetParameterType param, string value) + { + _values[param] = value; + return this; + } + + public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId) => + _values.TryGetValue(parameter, out var v) ? v : string.Empty; +} diff --git a/src/PepperDash.Core.Tests/Fakes/InMemoryCrestronDataStore.cs b/src/PepperDash.Core.Tests/Fakes/InMemoryCrestronDataStore.cs new file mode 100644 index 00000000..7cc2b618 --- /dev/null +++ b/src/PepperDash.Core.Tests/Fakes/InMemoryCrestronDataStore.cs @@ -0,0 +1,59 @@ +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Tests.Fakes; + +/// +/// In-memory ICrestronDataStore backed by a dictionary. +/// Use in unit tests to verify that keys are read from and written to the store correctly. +/// +public class InMemoryCrestronDataStore : ICrestronDataStore +{ + private readonly Dictionary _store = new(); + + public bool Initialized { get; private set; } + + public void InitStore() => Initialized = true; + + public bool TryGetLocalInt(string key, out int value) + { + if (_store.TryGetValue(key, out var raw) && raw is int i) + { + value = i; + return true; + } + value = 0; + return false; + } + + public bool SetLocalInt(string key, int value) + { + _store[key] = value; + return true; + } + + public bool SetLocalUint(string key, uint value) + { + _store[key] = (int)value; + return true; + } + + public bool TryGetLocalBool(string key, out bool value) + { + if (_store.TryGetValue(key, out var raw) && raw is bool b) + { + value = b; + return true; + } + value = false; + return false; + } + + public bool SetLocalBool(string key, bool value) + { + _store[key] = value; + return true; + } + + /// Seeds a key for testing read paths. + public void Seed(string key, object value) => _store[key] = value; +} diff --git a/src/PepperDash.Core.Tests/Logging/DebugServiceTests.cs b/src/PepperDash.Core.Tests/Logging/DebugServiceTests.cs new file mode 100644 index 00000000..13ae0f9e --- /dev/null +++ b/src/PepperDash.Core.Tests/Logging/DebugServiceTests.cs @@ -0,0 +1,171 @@ +using FluentAssertions; +using PepperDash.Core.Abstractions; +using PepperDash.Core.Tests.Fakes; +using Xunit; + +namespace PepperDash.Core.Tests.Logging; + +/// +/// Tests for Debug-related service interfaces and implementations. +/// These tests verify the behaviour of the abstractions in isolation (no Crestron SDK required). +/// +public class DebugServiceTests +{ + // ----------------------------------------------------------------------- + // ICrestronDataStore — InMemoryCrestronDataStore + // ----------------------------------------------------------------------- + + [Fact] + public void DataStore_InitStore_SetsInitializedFlag() + { + var store = new InMemoryCrestronDataStore(); + store.Initialized.Should().BeFalse("not yet initialized"); + + store.InitStore(); + + store.Initialized.Should().BeTrue(); + } + + [Fact] + public void DataStore_SetAndGetLocalInt_RoundTrips() + { + var store = new InMemoryCrestronDataStore(); + + store.SetLocalInt("MyKey", 42).Should().BeTrue(); + store.TryGetLocalInt("MyKey", out var value).Should().BeTrue(); + value.Should().Be(42); + } + + [Fact] + public void DataStore_TryGetLocalInt_ReturnsFalse_WhenKeyAbsent() + { + var store = new InMemoryCrestronDataStore(); + + store.TryGetLocalInt("Missing", out var value).Should().BeFalse(); + value.Should().Be(0); + } + + [Fact] + public void DataStore_SetAndGetLocalBool_RoundTrips() + { + var store = new InMemoryCrestronDataStore(); + + store.SetLocalBool("FlagKey", true).Should().BeTrue(); + store.TryGetLocalBool("FlagKey", out var value).Should().BeTrue(); + value.Should().BeTrue(); + } + + [Fact] + public void DataStore_TryGetLocalBool_ReturnsFalse_WhenKeyAbsent() + { + var store = new InMemoryCrestronDataStore(); + + store.TryGetLocalBool("Missing", out var value).Should().BeFalse(); + value.Should().BeFalse(); + } + + [Fact] + public void DataStore_SetLocalUint_CanBeReadBackAsInt() + { + var store = new InMemoryCrestronDataStore(); + + store.SetLocalUint("UintKey", 3u).Should().BeTrue(); + store.TryGetLocalInt("UintKey", out var value).Should().BeTrue(); + value.Should().Be(3); + } + + [Fact] + public void DataStore_Seed_AllowsTestSetupOfReadPaths() + { + var store = new InMemoryCrestronDataStore(); + store.Seed("MyLevel", 2); + + store.TryGetLocalInt("MyLevel", out var level).Should().BeTrue(); + level.Should().Be(2); + } + + // ----------------------------------------------------------------------- + // DebugServiceRegistration + // ----------------------------------------------------------------------- + + [Fact] + public void ServiceRegistration_Register_StoresAllThreeServices() + { + var env = new FakeCrestronEnvironment(); + var console = new NoOpCrestronConsole(); + var store = new InMemoryCrestronDataStore(); + + DebugServiceRegistration.Register(env, console, store); + + DebugServiceRegistration.Environment.Should().BeSameAs(env); + DebugServiceRegistration.Console.Should().BeSameAs(console); + DebugServiceRegistration.DataStore.Should().BeSameAs(store); + } + + [Fact] + public void ServiceRegistration_Register_AcceptsNullsWithoutThrowing() + { + var act = () => DebugServiceRegistration.Register(null, null, null); + act.Should().NotThrow(); + } + + // ----------------------------------------------------------------------- + // ICrestronEnvironment — FakeCrestronEnvironment + // ----------------------------------------------------------------------- + + [Fact] + public void FakeEnvironment_DefaultsToAppliance() + { + var env = new FakeCrestronEnvironment(); + env.DevicePlatform.Should().Be(DevicePlatform.Appliance); + } + + [Fact] + public void FakeEnvironment_RaiseProgramStatus_FiresEvent() + { + var env = new FakeCrestronEnvironment(); + ProgramStatusEventType? received = null; + env.ProgramStatusChanged += (_, e) => received = e.EventType; + + env.RaiseProgramStatus(ProgramStatusEventType.Stopping); + + received.Should().Be(ProgramStatusEventType.Stopping); + } + + [Fact] + public void FakeEnvironment_RaiseEthernetEvent_FiresEvent() + { + var env = new FakeCrestronEnvironment(); + EthernetEventType? received = null; + env.EthernetEventReceived += (_, e) => received = e.EthernetEventType; + + env.RaiseEthernetEvent(EthernetEventType.LinkUp, adapter: 0); + + received.Should().Be(EthernetEventType.LinkUp); + } + + // ----------------------------------------------------------------------- + // ICrestronConsole — CapturingCrestronConsole + // ----------------------------------------------------------------------- + + [Fact] + public void CapturingConsole_PrintLine_CapturesMessage() + { + var console = new CapturingCrestronConsole(); + + console.PrintLine("hello world"); + + console.Lines.Should().ContainSingle().Which.Should().Be("hello world"); + } + + [Fact] + public void CapturingConsole_AddNewConsoleCommand_RecordsCommandName() + { + var console = new CapturingCrestronConsole(); + + console.AddNewConsoleCommand(_ => { }, "appdebug", "Sets debug level", ConsoleAccessLevel.AccessOperator); + + console.RegisteredCommands.Should().ContainSingle() + .Which.Command.Should().Be("appdebug"); + } +} diff --git a/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj b/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj new file mode 100644 index 00000000..54b74d00 --- /dev/null +++ b/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj @@ -0,0 +1,25 @@ + + + net9.0 + enable + enable + false + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/src/PepperDash.Core/Adapters/CrestronConsoleAdapter.cs b/src/PepperDash.Core/Adapters/CrestronConsoleAdapter.cs new file mode 100644 index 00000000..8c4a6faf --- /dev/null +++ b/src/PepperDash.Core/Adapters/CrestronConsoleAdapter.cs @@ -0,0 +1,35 @@ +using System; +using Crestron.SimplSharp; +using PdCore = PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Adapters; + +/// +/// Production adapter — delegates ICrestronConsole calls to the real Crestron SDK. +/// +public sealed class CrestronConsoleAdapter : PdCore.ICrestronConsole +{ + public void PrintLine(string message) => CrestronConsole.PrintLine(message); + + public void Print(string message) => CrestronConsole.Print(message); + + public void ConsoleCommandResponse(string message) => + CrestronConsole.ConsoleCommandResponse(message); + + public void AddNewConsoleCommand( + Action callback, + string command, + string helpText, + PdCore.ConsoleAccessLevel accessLevel) + { + var crestronLevel = accessLevel switch + { + PdCore.ConsoleAccessLevel.AccessAdministrator => ConsoleAccessLevelEnum.AccessAdministrator, + PdCore.ConsoleAccessLevel.AccessProgrammer => ConsoleAccessLevelEnum.AccessProgrammer, + _ => ConsoleAccessLevelEnum.AccessOperator, + }; + + // Wrap Action in a lambda — Crestron's delegate is not a standard Action. + CrestronConsole.AddNewConsoleCommand(s => callback(s), command, helpText, crestronLevel); + } +} diff --git a/src/PepperDash.Core/Adapters/CrestronDataStoreAdapter.cs b/src/PepperDash.Core/Adapters/CrestronDataStoreAdapter.cs new file mode 100644 index 00000000..effed284 --- /dev/null +++ b/src/PepperDash.Core/Adapters/CrestronDataStoreAdapter.cs @@ -0,0 +1,42 @@ +using Crestron.SimplSharp.CrestronDataStore; +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Adapters; + +/// +/// Production adapter — delegates ICrestronDataStore calls to the real Crestron SDK. +/// +public sealed class CrestronDataStoreAdapter : ICrestronDataStore +{ + public void InitStore() => CrestronDataStoreStatic.InitCrestronDataStore(); + + public bool TryGetLocalInt(string key, out int value) + { + var err = CrestronDataStoreStatic.GetLocalIntValue(key, out value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } + + public bool SetLocalInt(string key, int value) + { + var err = CrestronDataStoreStatic.SetLocalIntValue(key, value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } + + public bool SetLocalUint(string key, uint value) + { + var err = CrestronDataStoreStatic.SetLocalUintValue(key, value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } + + public bool TryGetLocalBool(string key, out bool value) + { + var err = CrestronDataStoreStatic.GetLocalBoolValue(key, out value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } + + public bool SetLocalBool(string key, bool value) + { + var err = CrestronDataStoreStatic.SetLocalBoolValue(key, value); + return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS; + } +} diff --git a/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs b/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs new file mode 100644 index 00000000..752e9631 --- /dev/null +++ b/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs @@ -0,0 +1,68 @@ +using System; +using Crestron.SimplSharp; +using PdCore = PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Adapters; + +/// +/// Production adapter — delegates ICrestronEnvironment calls to the real Crestron SDK. +/// +public sealed class CrestronEnvironmentAdapter : PdCore.ICrestronEnvironment +{ + // Subscribe once in constructor and re-raise as our event types. + private event EventHandler? _programStatusChanged; + private event EventHandler? _ethernetEventReceived; + + public CrestronEnvironmentAdapter() + { + CrestronEnvironment.ProgramStatusEventHandler += type => + _programStatusChanged?.Invoke(this, new PdCore.ProgramStatusEventArgs(MapProgramStatus(type))); + + CrestronEnvironment.EthernetEventHandler += args => + _ethernetEventReceived?.Invoke(this, new PdCore.PepperDashEthernetEventArgs( + args.EthernetEventType == eEthernetEventType.LinkDown + ? PdCore.EthernetEventType.LinkDown + : PdCore.EthernetEventType.LinkUp, + (short)args.EthernetAdapter)); + } + + public PdCore.DevicePlatform DevicePlatform => + CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance + ? PdCore.DevicePlatform.Appliance + : PdCore.DevicePlatform.Server; + + public PdCore.RuntimeEnvironment RuntimeEnvironment => + CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro + ? PdCore.RuntimeEnvironment.SimplSharpPro + : PdCore.RuntimeEnvironment.Other; + + public string NewLine => CrestronEnvironment.NewLine; + + public uint ApplicationNumber => InitialParametersClass.ApplicationNumber; + + public uint RoomId => uint.TryParse(InitialParametersClass.RoomId, out var r) ? r : 0; + + public event EventHandler ProgramStatusChanged + { + add => _programStatusChanged += value; + remove => _programStatusChanged -= value; + } + + public event EventHandler EthernetEventReceived + { + add => _ethernetEventReceived += value; + remove => _ethernetEventReceived -= value; + } + + public string GetApplicationRootDirectory() => + Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory(); + + private static PdCore.ProgramStatusEventType MapProgramStatus(eProgramStatusEventType type) => + type switch + { + eProgramStatusEventType.Stopping => PdCore.ProgramStatusEventType.Stopping, + eProgramStatusEventType.Paused => PdCore.ProgramStatusEventType.Paused, + eProgramStatusEventType.Resumed => PdCore.ProgramStatusEventType.Resumed, + _ => PdCore.ProgramStatusEventType.Starting, + }; +} diff --git a/src/PepperDash.Core/Adapters/CrestronEthernetAdapter.cs b/src/PepperDash.Core/Adapters/CrestronEthernetAdapter.cs new file mode 100644 index 00000000..f2c80e5b --- /dev/null +++ b/src/PepperDash.Core/Adapters/CrestronEthernetAdapter.cs @@ -0,0 +1,39 @@ +using System; +using Crestron.SimplSharp; +using PepperDash.Core.Abstractions; + +namespace PepperDash.Core.Adapters; + +/// +/// Production adapter — delegates IEthernetHelper calls to the real Crestron SDK. +/// +public sealed class CrestronEthernetAdapter : IEthernetHelper +{ + public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId) + { + var crestronParam = parameter switch + { + EthernetParameterType.GetCurrentIpAddress => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, + EthernetParameterType.GetHostname => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, + EthernetParameterType.GetDomainName => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, + EthernetParameterType.GetLinkStatus => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS, + EthernetParameterType.GetCurrentDhcpState => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, + EthernetParameterType.GetCurrentIpMask => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, + EthernetParameterType.GetCurrentRouter => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, + EthernetParameterType.GetMacAddress => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, + EthernetParameterType.GetDnsServer => + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER, + _ => throw new ArgumentOutOfRangeException(nameof(parameter), parameter, null), + }; + + return CrestronEthernetHelper.GetEthernetParameter(crestronParam, ethernetAdapterId); + } +} diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 2e4b25e4..fb0483bb 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -11,6 +11,7 @@ using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronLogger; using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; +using PdCore = PepperDash.Core.Abstractions; using PepperDash.Core.Logging; using Serilog; using Serilog.Context; @@ -45,6 +46,13 @@ public static class Debug private static ILogger _logger; + // Injected service abstractions. Populated by DebugServiceRegistration.Register() + // before the Debug static constructor runs. Null when running on hardware without + // pre-registration (the static ctor falls back to the Crestron SDK directly). + private static PdCore.ICrestronEnvironment _environment; + private static PdCore.ICrestronConsole _console; + private static PdCore.ICrestronDataStore _dataStore; + private static readonly LoggingLevelSwitch consoleLoggingLevelSwitch; private static readonly LoggingLevelSwitch websocketLoggingLevelSwitch; @@ -107,7 +115,7 @@ public static class Debug /// /// Indicates whether the code is running on an appliance or not. Used to determine file paths and other appliance vs server differences /// - public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance; + public static bool IsRunningOnAppliance; // set in static constructor /// /// Version for the currently loaded PepperDashCore dll @@ -145,102 +153,114 @@ public static class Debug { try { - CrestronDataStoreStatic.InitCrestronDataStore(); + // Pick up services pre-registered by the composition root (or test setup). + // If null, fall back to direct Crestron SDK calls for production hardware. + _environment = PdCore.DebugServiceRegistration.Environment; + _console = PdCore.DebugServiceRegistration.Console; + _dataStore = PdCore.DebugServiceRegistration.DataStore; + + IsRunningOnAppliance = _environment?.DevicePlatform == PdCore.DevicePlatform.Appliance; + + _dataStore?.InitStore(); consoleDebugTimer = new Timer(defaultConsoleDebugTimeoutMin * 60000) { AutoReset = false }; consoleDebugTimer.Elapsed += (s, e) => { SetDebugLevel(LogEventLevel.Information); - CrestronConsole.ConsoleCommandResponse($"Console debug level reset to {LogEventLevel.Information} after timeout of {defaultConsoleDebugTimeoutMin} minutes"); + _console?.ConsoleCommandResponse($"Console debug level reset to {LogEventLevel.Information} after timeout of {defaultConsoleDebugTimeoutMin} minutes"); }; var defaultConsoleLevel = GetStoredLogEventLevel(LevelStoreKey); - var defaultWebsocketLevel = GetStoredLogEventLevel(WebSocketLevelStoreKey); - var defaultErrorLogLevel = GetStoredLogEventLevel(ErrorLogLevelStoreKey); - var defaultFileLogLevel = GetStoredLogEventLevel(FileLevelStoreKey); consoleLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultConsoleLevel); - websocketLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultWebsocketLevel); - errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel); - fileLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); - var logFilePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? - $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}app{InitialParametersClass.ApplicationNumber}{Path.DirectorySeparatorChar}global-log.log" : - $@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}room{InitialParametersClass.RoomId}{Path.DirectorySeparatorChar}global-log.log"; + var appRoot = _environment?.GetApplicationRootDirectory() + ?? System.IO.Path.GetTempPath(); + var sep = System.IO.Path.DirectorySeparatorChar; + var appNum = _environment?.ApplicationNumber ?? 0; + var roomId = _environment?.RoomId ?? 0; - CrestronConsole.PrintLine($"Saving log files to {logFilePath}"); + var logFilePath = IsRunningOnAppliance + ? $"{appRoot}{sep}user{sep}debug{sep}app{appNum}{sep}global-log.log" + : $"{appRoot}{sep}user{sep}debug{sep}room{roomId}{sep}global-log.log"; - var errorLogTemplate = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance - ? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}" - : "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; + _console?.PrintLine($"Saving log files to {logFilePath}"); + // Build the base Serilog pipeline — sinks that require the Crestron SDK are + // added conditionally so the logger remains usable in test environments. _defaultLoggerConfiguration = new LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() - .Enrich.With(new CrestronEnricher()) .WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: consoleLoggingLevelSwitch) .WriteTo.Sink(websocketSink, levelSwitch: websocketLoggingLevelSwitch) - .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: errorLogLevelSwitch) .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug, - retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, + retainedFileCountLimit: IsRunningOnAppliance ? 30 : 60, levelSwitch: fileLoggingLevelSwitch ); - // Instantiate the root logger - _loggerConfiguration = _defaultLoggerConfiguration; - - _logger = _loggerConfiguration.CreateLogger(); - // Get the assembly version and print it to console and the log - GetVersion(); - - string msg = $"[App {InitialParametersClass.ApplicationNumber}] Using PepperDash_Core v{PepperDashCoreVersion}"; - - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server) + // Add Crestron-specific enricher and error-log sink only when running on hardware. + if (_environment != null) { - msg = $"[Room {InitialParametersClass.RoomId}] Using PepperDash_Core v{PepperDashCoreVersion}"; + var errorLogTemplate = IsRunningOnAppliance + ? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}" + : "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"; + + _defaultLoggerConfiguration + .Enrich.With(new CrestronEnricher()) + .WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: errorLogLevelSwitch); } - CrestronConsole.PrintLine(msg); + _loggerConfiguration = _defaultLoggerConfiguration; + _logger = _loggerConfiguration.CreateLogger(); + GetVersion(); + + string msg = IsRunningOnAppliance + ? $"[App {appNum}] Using PepperDash_Core v{PepperDashCoreVersion}" + : $"[Room {roomId}] Using PepperDash_Core v{PepperDashCoreVersion}"; + + _console?.PrintLine(msg); LogMessage(LogEventLevel.Information, msg); IncludedExcludedKeys = new Dictionary(); - if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) + if (_environment?.RuntimeEnvironment == PdCore.RuntimeEnvironment.SimplSharpPro) { - // Add command to console - CrestronConsole.AddNewConsoleCommand(SetDoNotLoadOnNextBootFromConsole, "donotloadonnextboot", - "donotloadonnextboot:P [true/false]: Should the application load on next boot", ConsoleAccessLevelEnum.AccessOperator); + _console?.AddNewConsoleCommand(SetDoNotLoadOnNextBootFromConsole, "donotloadonnextboot", + "donotloadonnextboot:P [true/false]: Should the application load on next boot", + PdCore.ConsoleAccessLevel.AccessOperator); - CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", + _console?.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", "appdebug:P [0-5]: Sets the application's console debug message level", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", - "appdebuglog:P [all] Use \"all\" for full log.", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronLogger.Clear(false), "appdebugclear", - "appdebugclear:P Clears the current custom log", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter", - "appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator); - } + PdCore.ConsoleAccessLevel.AccessOperator); - // CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; + _console?.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", + "appdebuglog:P [all] Use \"all\" for full log.", + PdCore.ConsoleAccessLevel.AccessOperator); + + _console?.AddNewConsoleCommand(s => CrestronLogger.Clear(false), "appdebugclear", + "appdebugclear:P Clears the current custom log", + PdCore.ConsoleAccessLevel.AccessOperator); + + _console?.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter", + "appdebugfilter [params]", + PdCore.ConsoleAccessLevel.AccessOperator); + } DoNotLoadConfigOnNextBoot = GetDoNotLoadOnNextBoot(); if (DoNotLoadConfigOnNextBoot) - CrestronConsole.PrintLine(string.Format("Program {0} will not load config after next boot. Use console command go:{0} to load the config manually", InitialParametersClass.ApplicationNumber)); + _console?.PrintLine($"Program {appNum} will not load config after next boot. Use console command go:{appNum} to load the config manually"); consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) => { @@ -250,18 +270,20 @@ public static class Debug catch (Exception ex) { // _logger may not have been initialized yet — do not call LogError here. - CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); + // _console may also be null; fall back to CrestronConsole as last resort. + try { _console?.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); } + catch { CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); } } } private static bool GetDoNotLoadOnNextBoot() { - var err = CrestronDataStoreStatic.GetLocalBoolValue(DoNotLoadOnNextBootKey, out var doNotLoad); + if (_dataStore == null) return false; - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + if (!_dataStore.TryGetLocalBool(DoNotLoadOnNextBootKey, out var doNotLoad)) { - LogError("Error retrieving DoNotLoadOnNextBoot value: {err}", err); - doNotLoad = false; + LogError("Error retrieving DoNotLoadOnNextBoot value"); + return false; } return doNotLoad; @@ -294,17 +316,17 @@ public static class Debug { try { - var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel); + if (_dataStore == null) return LogEventLevel.Information; - if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + if (!_dataStore.TryGetLocalInt(levelStoreKey, out int logLevel)) { - CrestronConsole.Print($"Unable to retrieve stored log level for {levelStoreKey}.\r\nError: {result}.\r\nSetting level to {LogEventLevel.Information}\r\n"); + _console?.Print($"Unable to retrieve stored log level for {levelStoreKey}. Setting level to {LogEventLevel.Information}\r\n"); return LogEventLevel.Information; } if (logLevel < 0 || logLevel > 5) { - CrestronConsole.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); + _console?.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); return LogEventLevel.Information; } @@ -312,7 +334,7 @@ public static class Debug } catch (Exception ex) { - CrestronConsole.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); + _console?.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); return LogEventLevel.Information; } } @@ -369,7 +391,7 @@ public static class Debug { if (levelString.Trim() == "?") { - CrestronConsole.ConsoleCommandResponse( + _console?.ConsoleCommandResponse( "Used to set the minimum level of debug messages to be printed to the console:\r\n" + "[LogLevel] [TimeoutInMinutes]\r\n" + @@ -386,7 +408,7 @@ public static class Debug if (string.IsNullOrEmpty(levelString.Trim())) { - CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", consoleLoggingLevelSwitch.MinimumLevel); + _console?.ConsoleCommandResponse($"AppDebug level = {consoleLoggingLevelSwitch.MinimumLevel}"); return; } @@ -406,7 +428,7 @@ public static class Debug { if (levelInt < 0 || levelInt > 5) { - CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5"); + _console?.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5"); return; } SetDebugLevel((uint)levelInt); @@ -420,11 +442,11 @@ public static class Debug return; } - CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); + _console?.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); } catch { - CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); + _console?.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); } } @@ -439,7 +461,7 @@ public static class Debug { logLevel = LogEventLevel.Information; - CrestronConsole.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}"); + _console?.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}"); SetDebugLevel(logLevel, timeout); } @@ -460,17 +482,14 @@ public static class Debug consoleLoggingLevelSwitch.MinimumLevel = level; - CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}\r\n", - InitialParametersClass.ApplicationNumber, consoleLoggingLevelSwitch.MinimumLevel); + var appNum = _environment?.ApplicationNumber ?? 0; + _console?.ConsoleCommandResponse($"[Application {appNum}], Debug level set to {consoleLoggingLevelSwitch.MinimumLevel}\r\n"); + _console?.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); - CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int)level}"); - - var err = CrestronDataStoreStatic.SetLocalIntValue(LevelStoreKey, (int)level); - - CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}"); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - CrestronConsole.PrintLine($"Error saving console debug level setting: {err}"); + if (_dataStore != null && !_dataStore.SetLocalInt(LevelStoreKey, (int)level)) + _console?.PrintLine($"Error saving console debug level setting"); + else + _console?.ConsoleCommandResponse($"Store result: {(int)level}"); } /// @@ -481,10 +500,8 @@ public static class Debug { websocketLoggingLevelSwitch.MinimumLevel = level; - var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {error}", err); + if (_dataStore != null && !_dataStore.SetLocalUint(WebSocketLevelStoreKey, (uint)level)) + LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting"); LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", websocketLoggingLevelSwitch.MinimumLevel); } @@ -498,10 +515,8 @@ public static class Debug { errorLogLevelSwitch.MinimumLevel = level; - var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting: {error}", err); + if (_dataStore != null && !_dataStore.SetLocalUint(ErrorLogLevelStoreKey, (uint)level)) + LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting"); LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", errorLogLevelSwitch.MinimumLevel); } @@ -513,10 +528,8 @@ public static class Debug { fileLoggingLevelSwitch.MinimumLevel = level; - var err = CrestronDataStoreStatic.SetLocalUintValue(FileLevelStoreKey, (uint)level); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogMessage(LogEventLevel.Information, "Error saving File debug level setting: {error}", err); + if (_dataStore != null && !_dataStore.SetLocalUint(FileLevelStoreKey, (uint)level)) + LogMessage(LogEventLevel.Information, "Error saving File debug level setting"); LogMessage(LogEventLevel.Information, "File debug level set to {0}", fileLoggingLevelSwitch.MinimumLevel); } @@ -531,7 +544,7 @@ public static class Debug { if (string.IsNullOrEmpty(stateString.Trim())) { - CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadConfigOnNextBoot); + _console?.ConsoleCommandResponse($"DoNotLoadOnNextBoot = {DoNotLoadConfigOnNextBoot}"); return; } @@ -539,7 +552,7 @@ public static class Debug } catch { - CrestronConsole.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]"); + _console?.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]"); } } @@ -655,10 +668,8 @@ public static class Debug { DoNotLoadConfigOnNextBoot = state; - var err = CrestronDataStoreStatic.SetLocalBoolValue(DoNotLoadOnNextBootKey, state); - - if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - LogError("Error saving console debug level setting: {err}", err); + if (_dataStore != null && !_dataStore.SetLocalBool(DoNotLoadOnNextBootKey, state)) + LogError("Error saving DoNotLoadConfigOnNextBoot setting"); LogInformation("Do Not Load Config on Next Boot set to {state}", DoNotLoadConfigOnNextBoot); } @@ -668,9 +679,10 @@ public static class Debug /// public static void ShowDebugLog(string s) { + if (_environment == null) return; // CrestronLogger not available in test environments var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all"); foreach (var l in loglist) - CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); + _console?.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); } /// diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj index 6ff5dd71..4cd091ef 100644 --- a/src/PepperDash.Core/PepperDash.Core.csproj +++ b/src/PepperDash.Core/PepperDash.Core.csproj @@ -50,6 +50,9 @@ + + + diff --git a/src/PepperDash.Essentials.Core.Tests/Config/ConfigServiceFakesTests.cs b/src/PepperDash.Essentials.Core.Tests/Config/ConfigServiceFakesTests.cs new file mode 100644 index 00000000..1359a450 --- /dev/null +++ b/src/PepperDash.Essentials.Core.Tests/Config/ConfigServiceFakesTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using PepperDash.Core.Abstractions; +using PepperDash.Core.Tests.Fakes; +using Xunit; + +namespace PepperDash.Essentials.Core.Tests.Config; + +/// +/// Tests for the configuration loading abstractions. +/// These verify behaviour of the test fakes and interfaces independently of +/// any Crestron SDK types (ConfigReader itself will be tested here once it +/// is migrated from Crestron.SimplSharp.CrestronIO to System.IO — see plan Phase 4). +/// +public class ConfigServiceFakesTests +{ + [Fact] + public void DataStore_MultipleKeys_AreStoredIndependently() + { + var store = new InMemoryCrestronDataStore(); + store.InitStore(); + + store.SetLocalInt("KeyA", 1); + store.SetLocalInt("KeyB", 2); + + store.TryGetLocalInt("KeyA", out var a); + store.TryGetLocalInt("KeyB", out var b); + + a.Should().Be(1); + b.Should().Be(2); + } + + [Fact] + public void DataStore_OverwriteKey_ReturnsNewValue() + { + var store = new InMemoryCrestronDataStore(); + store.SetLocalInt("Level", 1); + store.SetLocalInt("Level", 5); + + store.TryGetLocalInt("Level", out var level); + level.Should().Be(5); + } + + [Fact] + public void FakeEnvironment_CanBeConfiguredForServer() + { + var env = new FakeCrestronEnvironment + { + DevicePlatform = DevicePlatform.Server, + ApplicationNumber = 1, + RoomId = 42, + }; + + env.DevicePlatform.Should().Be(DevicePlatform.Server); + env.RoomId.Should().Be(42u); + } + + [Fact] + public void FakeEthernetHelper_SeedAndRetrieve() + { + var eth = new FakeEthernetHelper() + .Seed(EthernetParameterType.GetCurrentIpAddress, "192.168.1.100") + .Seed(EthernetParameterType.GetHostname, "MC4-TEST"); + + eth.GetEthernetParameter(EthernetParameterType.GetCurrentIpAddress, 0) + .Should().Be("192.168.1.100"); + + eth.GetEthernetParameter(EthernetParameterType.GetHostname, 0) + .Should().Be("MC4-TEST"); + + eth.GetEthernetParameter(EthernetParameterType.GetDomainName, 0) + .Should().BeEmpty("unseeded parameter should return empty string"); + } +} diff --git a/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj b/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj new file mode 100644 index 00000000..429afd56 --- /dev/null +++ b/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj @@ -0,0 +1,27 @@ + + + net9.0 + enable + enable + false + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 63f4160e..ffb8a2d3 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -7,6 +7,8 @@ using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.Diagnostics; using PepperDash.Core; +using PepperDash.Core.Abstractions; +using PepperDash.Core.Adapters; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; @@ -46,6 +48,14 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig { try { + // Register Crestron service adapters BEFORE the first reference to Debug, + // so that Debug's static constructor uses these implementations instead of + // calling the Crestron SDK statics directly. + DebugServiceRegistration.Register( + new CrestronEnvironmentAdapter(), + new CrestronConsoleAdapter(), + new CrestronDataStoreAdapter()); + Crestron.SimplSharpPro.CrestronThread.Thread.MaxNumberOfUserThreads = 400; Global.ControlSystem = this; From 37961b9eacd3824a5e8ee6838fcf8f69982feb4a Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 13:21:15 -0600 Subject: [PATCH 033/161] feat: Enhance device testing and environment handling with new interfaces and fakes --- .../ICrestronEnvironment.cs | 7 + .../Devices/DeviceTests.cs | 323 ++++++++++++++++++ .../Fakes/CrestronEnvironmentFakes.cs | 3 + .../PepperDash.Core.Tests.csproj | 4 + src/PepperDash.Core.Tests/TestInitializer.cs | 29 ++ .../Adapters/CrestronEnvironmentAdapter.cs | 3 + src/PepperDash.Core/Device.cs | 8 +- src/PepperDash.Core/Logging/Debug.cs | 44 ++- .../Logging/DebugWebsocketSink.cs | 15 +- .../Messengers/DeviceStateMessageBase.cs | 14 - .../Messengers/MessengerBase.cs | 4 - 11 files changed, 421 insertions(+), 33 deletions(-) create mode 100644 src/PepperDash.Core.Tests/Devices/DeviceTests.cs create mode 100644 src/PepperDash.Core.Tests/TestInitializer.cs diff --git a/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs b/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs index c31fb3f6..1e73c83f 100644 --- a/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs +++ b/src/PepperDash.Core.Abstractions/ICrestronEnvironment.cs @@ -29,6 +29,13 @@ public interface ICrestronEnvironment /// Gets the application root directory path. string GetApplicationRootDirectory(); + + /// + /// Returns true when running on real Crestron hardware. + /// Returns false in test / dev environments so that SDK-specific + /// sinks and enrichers can be safely skipped. + /// + bool IsHardwareRuntime { get; } } /// diff --git a/src/PepperDash.Core.Tests/Devices/DeviceTests.cs b/src/PepperDash.Core.Tests/Devices/DeviceTests.cs new file mode 100644 index 00000000..2450e401 --- /dev/null +++ b/src/PepperDash.Core.Tests/Devices/DeviceTests.cs @@ -0,0 +1,323 @@ +using FluentAssertions; +using PepperDash.Core; +using Xunit; + +namespace PepperDash.Core.Tests.Devices; + +/// +/// Tests for — the base class for all PepperDash devices. +/// These run without Crestron hardware; Debug is initialized with fakes via TestInitializer. +/// +public class DeviceTests +{ + // ----------------------------------------------------------------------- + // Construction + // ----------------------------------------------------------------------- + + [Fact] + public void Constructor_SingleArg_SetsKey() + { + var device = new Device("my-device"); + + device.Key.Should().Be("my-device"); + } + + [Fact] + public void Constructor_SingleArg_SetsNameToEmpty() + { + var device = new Device("my-device"); + + device.Name.Should().BeEmpty(); + } + + [Fact] + public void Constructor_TwoArg_SetsKeyAndName() + { + var device = new Device("my-device", "My Device"); + + device.Key.Should().Be("my-device"); + device.Name.Should().Be("My Device"); + } + + [Fact] + public void Constructor_KeyWithDot_StillSetsKey() + { + // The dot triggers a debug log warning but must not prevent construction. + var device = new Device("parent.child"); + + device.Key.Should().Be("parent.child"); + } + + // ----------------------------------------------------------------------- + // ToString + // ----------------------------------------------------------------------- + + [Fact] + public void ToString_WithName_FormatsKeyDashName() + { + var device = new Device("cam-01", "Front Camera"); + + device.ToString().Should().Be("cam-01 - Front Camera"); + } + + [Fact] + public void ToString_WithoutName_UsesDashPlaceholder() + { + var device = new Device("cam-01"); + + device.ToString().Should().Be("cam-01 - ---"); + } + + // ----------------------------------------------------------------------- + // DefaultDevice + // ----------------------------------------------------------------------- + + [Fact] + public void DefaultDevice_IsNotNull() + { + Device.DefaultDevice.Should().NotBeNull(); + } + + [Fact] + public void DefaultDevice_HasKeyDefault() + { + Device.DefaultDevice.Key.Should().Be("Default"); + } + + // ----------------------------------------------------------------------- + // CustomActivate / Activate / Deactivate / Initialize + // ----------------------------------------------------------------------- + + [Fact] + public void CustomActivate_DefaultReturnTrue() + { + var device = new TestDevice("d1"); + + device.CallCustomActivate().Should().BeTrue(); + } + + [Fact] + public void Deactivate_DefaultReturnsTrue() + { + var device = new Device("d1"); + + device.Deactivate().Should().BeTrue(); + } + + [Fact] + public void Activate_CallsCustomActivate_AndReturnsItsResult() + { + var stub = new ActivateTrackingDevice("d1", result: false); + + stub.Activate().Should().BeFalse(); + stub.CustomActivateCalled.Should().BeTrue(); + } + + [Fact] + public void Activate_TrueWhenCustomActivateReturnsTrue() + { + var stub = new ActivateTrackingDevice("d1", result: true); + + stub.Activate().Should().BeTrue(); + } + + [Fact] + public void Initialize_DoesNotThrow() + { + var device = new TestDevice("d1"); + var act = () => device.CallInitialize(); + + act.Should().NotThrow(); + } + + // ----------------------------------------------------------------------- + // PreActivate + // ----------------------------------------------------------------------- + + [Fact] + public void PreActivate_NoActions_DoesNotThrow() + { + var device = new TestDevice("d1"); + var act = () => device.PreActivate(); + + act.Should().NotThrow(); + } + + [Fact] + public void PreActivate_RunsRegisteredActionsInOrder() + { + var device = new TestDevice("d1"); + var order = new List(); + + device.AddPreActivationAction(() => order.Add(1)); + device.AddPreActivationAction(() => order.Add(2)); + device.AddPreActivationAction(() => order.Add(3)); + + device.PreActivate(); + + order.Should().Equal(1, 2, 3); + } + + [Fact] + public void PreActivate_ContinuesAfterFaultingAction() + { + var device = new TestDevice("d1"); + var reached = false; + + device.AddPreActivationAction(() => throw new InvalidOperationException("boom")); + device.AddPreActivationAction(() => reached = true); + + var act = () => device.PreActivate(); + + act.Should().NotThrow("exceptions in individual actions must be caught internally"); + reached.Should().BeTrue("actions after a faulting action must still run"); + } + + // ----------------------------------------------------------------------- + // PostActivate + // ----------------------------------------------------------------------- + + [Fact] + public void PostActivate_NoActions_DoesNotThrow() + { + var device = new TestDevice("d1"); + var act = () => device.PostActivate(); + + act.Should().NotThrow(); + } + + [Fact] + public void PostActivate_RunsRegisteredActionsInOrder() + { + var device = new TestDevice("d1"); + var order = new List(); + + device.AddPostActivationAction(() => order.Add(1)); + device.AddPostActivationAction(() => order.Add(2)); + + device.PostActivate(); + + order.Should().Equal(1, 2); + } + + [Fact] + public void PostActivate_ContinuesAfterFaultingAction() + { + var device = new TestDevice("d1"); + var reached = false; + + device.AddPostActivationAction(() => throw new Exception("boom")); + device.AddPostActivationAction(() => reached = true); + + var act = () => device.PostActivate(); + + act.Should().NotThrow(); + reached.Should().BeTrue(); + } + + // ----------------------------------------------------------------------- + // Pre and Post actions are independent lists + // ----------------------------------------------------------------------- + + [Fact] + public void PreActivationActions_DoNotRunOnPostActivate() + { + var device = new TestDevice("d1"); + var preRan = false; + + device.AddPreActivationAction(() => preRan = true); + device.PostActivate(); + + preRan.Should().BeFalse(); + } + + [Fact] + public void PostActivationActions_DoNotRunOnPreActivate() + { + var device = new TestDevice("d1"); + var postRan = false; + + device.AddPostActivationAction(() => postRan = true); + device.PreActivate(); + + postRan.Should().BeFalse(); + } + + // ----------------------------------------------------------------------- + // OnFalse + // ----------------------------------------------------------------------- + + [Fact] + public void OnFalse_FiresAction_WhenBoolIsFalse() + { + var device = new Device("d1"); + var fired = false; + + device.OnFalse(false, () => fired = true); + + fired.Should().BeTrue(); + } + + [Fact] + public void OnFalse_DoesNotFireAction_WhenBoolIsTrue() + { + var device = new Device("d1"); + var fired = false; + + device.OnFalse(true, () => fired = true); + + fired.Should().BeFalse(); + } + + [Fact] + public void OnFalse_DoesNotFireAction_ForNonBoolType() + { + var device = new Device("d1"); + var fired = false; + + device.OnFalse("not a bool", () => fired = true); + device.OnFalse(0, () => fired = true); + device.OnFalse(null!, () => fired = true); + + fired.Should().BeFalse(); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + /// + /// Exposes protected Device members so test methods can call them directly. + /// + private class TestDevice : Device + { + public TestDevice(string key) : base(key) { } + public TestDevice(string key, string name) : base(key, name) { } + + public void AddPreActivationAction(Action act) => base.AddPreActivationAction(act); + public void AddPostActivationAction(Action act) => base.AddPostActivationAction(act); + public bool CallCustomActivate() => base.CustomActivate(); + public void CallInitialize() => base.Initialize(); + } + + /// + /// Records whether CustomActivate was invoked and returns a configured result. + /// Used to verify Activate() correctly delegates to CustomActivate(). + /// + private sealed class ActivateTrackingDevice : Device + { + private readonly bool _result; + public bool CustomActivateCalled { get; private set; } + + public ActivateTrackingDevice(string key, bool result = true) : base(key) + { + _result = result; + } + + protected override bool CustomActivate() + { + CustomActivateCalled = true; + return _result; + } + } +} diff --git a/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs b/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs index 8070f849..1792206d 100644 --- a/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs +++ b/src/PepperDash.Core.Tests/Fakes/CrestronEnvironmentFakes.cs @@ -19,6 +19,9 @@ public class FakeCrestronEnvironment : ICrestronEnvironment public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath(); + /// + public bool IsHardwareRuntime => false; + /// Simulates a program status event for tests. public void RaiseProgramStatus(ProgramStatusEventType type) => ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type)); diff --git a/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj b/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj index 54b74d00..9e045a89 100644 --- a/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj +++ b/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj @@ -21,5 +21,9 @@ + + diff --git a/src/PepperDash.Core.Tests/TestInitializer.cs b/src/PepperDash.Core.Tests/TestInitializer.cs new file mode 100644 index 00000000..6455ea59 --- /dev/null +++ b/src/PepperDash.Core.Tests/TestInitializer.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; +using PepperDash.Core.Abstractions; +using PepperDash.Core.Tests.Fakes; + +namespace PepperDash.Core.Tests; + +/// +/// Runs once before any type in this assembly is accessed. +/// Registers fake Crestron service implementations with +/// so that the Debug static constructor uses them instead of the real Crestron SDK. +/// This must remain a module initializer (not a test fixture) because the static constructor +/// fires the first time any type in PepperDash.Core is referenced — before xUnit +/// has a chance to run fixture setup code. +/// +internal static class TestInitializer +{ + [ModuleInitializer] + internal static void Initialize() + { + DebugServiceRegistration.Register( + new FakeCrestronEnvironment + { + DevicePlatform = DevicePlatform.Server, // avoids any appliance-only code paths + RuntimeEnvironment = RuntimeEnvironment.Other, // skips console command registration + }, + new NoOpCrestronConsole(), + new InMemoryCrestronDataStore()); + } +} diff --git a/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs b/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs index 752e9631..62002d73 100644 --- a/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs +++ b/src/PepperDash.Core/Adapters/CrestronEnvironmentAdapter.cs @@ -57,6 +57,9 @@ public sealed class CrestronEnvironmentAdapter : PdCore.ICrestronEnvironment public string GetApplicationRootDirectory() => Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory(); + /// + public bool IsHardwareRuntime => true; + private static PdCore.ProgramStatusEventType MapProgramStatus(eProgramStatusEventType type) => type switch { diff --git a/src/PepperDash.Core/Device.cs b/src/PepperDash.Core/Device.cs index 70d4d97f..ff436b68 100644 --- a/src/PepperDash.Core/Device.cs +++ b/src/PepperDash.Core/Device.cs @@ -75,7 +75,7 @@ public class Device : IKeyName /// Adds a pre activation action /// /// - public void AddPreActivationAction(Action act) + protected void AddPreActivationAction(Action act) { if (_PreActivationActions == null) _PreActivationActions = new List(); @@ -89,7 +89,7 @@ public class Device : IKeyName /// /// AddPostActivationAction method /// - public void AddPostActivationAction(Action act) + protected void AddPostActivationAction(Action act) { if (_PostActivationActions == null) _PostActivationActions = new List(); @@ -156,7 +156,7 @@ public class Device : IKeyName /// /// CustomActivate method /// - public virtual bool CustomActivate() { return true; } + protected virtual bool CustomActivate() { return true; } /// /// Call to deactivate device - unlink events, etc. Overriding classes do not @@ -168,7 +168,7 @@ public class Device : IKeyName /// /// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize() /// - public virtual void Initialize() + protected virtual void Initialize() { } diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index fb0483bb..97a6e93e 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -96,7 +96,7 @@ public static class Debug /// /// The name of the file containing the current debug settings. /// - public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber); + public static string FileName = "app0Debug.json"; // default; updated in static ctor using _environment.ApplicationNumber /// /// Debug level to set for a given program. @@ -161,6 +161,9 @@ public static class Debug IsRunningOnAppliance = _environment?.DevicePlatform == PdCore.DevicePlatform.Appliance; + // Update FileName now that _environment is available (avoids Crestron SDK ref in field initializer). + FileName = $"app{_environment?.ApplicationNumber ?? 0}Debug.json"; + _dataStore?.InitStore(); consoleDebugTimer = new Timer(defaultConsoleDebugTimeoutMin * 60000) { AutoReset = false }; @@ -180,7 +183,7 @@ public static class Debug errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel); fileLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel); - websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); + websocketSink = TryCreateWebsocketSink(); var appRoot = _environment?.GetApplicationRootDirectory() ?? System.IO.Path.GetTempPath(); @@ -200,7 +203,6 @@ public static class Debug .MinimumLevel.Verbose() .Enrich.FromLogContext() .WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: consoleLoggingLevelSwitch) - .WriteTo.Sink(websocketSink, levelSwitch: websocketLoggingLevelSwitch) .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug, @@ -208,8 +210,12 @@ public static class Debug levelSwitch: fileLoggingLevelSwitch ); - // Add Crestron-specific enricher and error-log sink only when running on hardware. - if (_environment != null) + // Websocket sink is null when DebugWebsocketSink failed to construct (e.g. test env). + if (websocketSink != null) + _defaultLoggerConfiguration.WriteTo.Sink(websocketSink, levelSwitch: websocketLoggingLevelSwitch); + + // Add Crestron-specific enricher and error-log sink only on real hardware. + if (_environment?.IsHardwareRuntime == true) { var errorLogTemplate = IsRunningOnAppliance ? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}" @@ -271,8 +277,34 @@ public static class Debug { // _logger may not have been initialized yet — do not call LogError here. // _console may also be null; fall back to CrestronConsole as last resort. + // IMPORTANT: this catch block must not throw — any exception escaping a static + // constructor permanently faults the type, making the entire class unusable. try { _console?.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); } - catch { CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); } + catch + { + try { CrestronConsole.PrintLine($"Exception in Debug static constructor: {ex.Message}\r\n{ex.StackTrace}"); } + catch { /* CrestronConsole unavailable (test/dev env) — swallow to keep type initializer healthy */ } + } + } + finally + { + // Guarantee _logger is never null — all Debug.Log* calls are safe even if the + // ctor failed partway through (e.g. on a dev machine without Crestron hardware). + _logger ??= new LoggerConfiguration().MinimumLevel.Fatal().CreateLogger(); + } + } + + /// Creates the WebSocket sink, returning null if construction fails in a test/dev environment. + private static DebugWebsocketSink? TryCreateWebsocketSink() + { + try + { + return new DebugWebsocketSink(new JsonFormatter(renderMessage: true)); + } + catch + { + _console?.PrintLine("DebugWebsocketSink could not be created in this environment; websocket logging disabled."); + return null; } } diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 6f1aee18..52d61896 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -91,13 +91,18 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed if (!File.Exists(CertPath)) CreateCert(); - CrestronEnvironment.ProgramStatusEventHandler += type => + try { - if (type == eProgramStatusEventType.Stopping) + CrestronEnvironment.ProgramStatusEventHandler += type => { - StopServer(); - } - }; + if (type == eProgramStatusEventType.Stopping) + StopServer(); + }; + } + catch + { + // CrestronEnvironment is not available in test / dev environments — safe to skip. + } } private static void CreateCert() diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs index 87f19e3f..1e538a31 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs @@ -8,20 +8,6 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class DeviceStateMessageBase : DeviceMessageBase { - /// - /// The interfaces implmented by the device sending the messsage - /// - [JsonProperty("interfaces")] - public List Interfaces { get; private set; } - - /// - /// Sets the interfaces implemented by the device sending the message - /// - /// - public void SetInterfaces(List interfaces) - { - Interfaces = interfaces; - } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index 9d22803d..fbbaa5e5 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -258,8 +258,6 @@ namespace PepperDash.Essentials.AppServer.Messengers throw new ArgumentNullException("device"); } - message.SetInterfaces(_deviceInterfaces); - message.Key = _device.Key; message.Name = _device.Name; @@ -285,8 +283,6 @@ namespace PepperDash.Essentials.AppServer.Messengers { try { - deviceState.SetInterfaces(_deviceInterfaces); - deviceState.Key = _device.Key; deviceState.Name = _device.Name; From 5dd6d18fcc190c2c82042cf7db688b1cb181a748 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 15:23:49 -0600 Subject: [PATCH 034/161] feat: Update CI workflow and project files for improved testing and documentation generation --- ...sentialsPlugins-builds-4-series-caller.yml | 17 +++ PepperDash.Essentials.4Series.sln | 122 ++++++++++++++++++ .../PepperDash.Core.Tests.csproj | 1 + src/PepperDash.Core/Device.cs | 1 - .../PepperDash.Essentials.Core.Tests.csproj | 1 + 5 files changed, 141 insertions(+), 1 deletion(-) diff --git a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml index 291e9371..660f7294 100644 --- a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml +++ b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml @@ -6,9 +6,26 @@ on: - '**' jobs: + # runTests: + # uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-tests.yml@main + # secrets: inherit + runTests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 9.0.x + - name: Restore dependencies + working-directory: . + run: dotnet restore + - name: Test + working-directory: . + run: dotnet test --verbosity normal --no-restore --collect:"xplat code coverage" getVersion: uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-getversion.yml@main secrets: inherit + needs: runTests build-4Series: uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-4Series-builds.yml@main secrets: inherit diff --git a/PepperDash.Essentials.4Series.sln b/PepperDash.Essentials.4Series.sln index 7423c50a..58293b87 100644 --- a/PepperDash.Essentials.4Series.sln +++ b/PepperDash.Essentials.4Series.sln @@ -36,49 +36,169 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core.Tests", "src\PepperDash.Core.Tests\PepperDash.Core.Tests.csproj", "{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Core.Tests", "src\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{F508E0BA-E885-424F-9D4C-359CF0011DEF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU + Debug 4.7.2|x64 = Debug 4.7.2|x64 + Debug 4.7.2|x86 = Debug 4.7.2|x86 Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x64.ActiveCfg = Debug|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x64.Build.0 = Debug|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x86.ActiveCfg = Debug|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x86.Build.0 = Debug|Any CPU {53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.ActiveCfg = Release|Any CPU {53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.Build.0 = Release|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x64.ActiveCfg = Release|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x64.Build.0 = Release|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x86.ActiveCfg = Release|Any CPU + {53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x86.Build.0 = Release|Any CPU {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x64.ActiveCfg = Debug|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x64.Build.0 = Debug|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x86.ActiveCfg = Debug|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x86.Build.0 = Debug|Any CPU {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.Build.0 = Release|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x64.ActiveCfg = Release|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x64.Build.0 = Release|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x86.ActiveCfg = Release|Any CPU + {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x86.Build.0 = Release|Any CPU {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x64.Build.0 = Debug|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x86.Build.0 = Debug|Any CPU {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.Build.0 = Release|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x64.ActiveCfg = Release|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x64.Build.0 = Release|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x86.ActiveCfg = Release|Any CPU + {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x86.Build.0 = Release|Any CPU {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x64.ActiveCfg = Debug|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x64.Build.0 = Debug|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x86.ActiveCfg = Debug|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x86.Build.0 = Debug|Any CPU {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.Build.0 = Release|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x64.ActiveCfg = Release|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x64.Build.0 = Release|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x86.ActiveCfg = Release|Any CPU + {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x86.Build.0 = Release|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x64.ActiveCfg = Debug|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x64.Build.0 = Debug|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x86.Build.0 = Debug|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.ActiveCfg = Release|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.Build.0 = Release|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Release|x64.ActiveCfg = Release|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Release|x64.Build.0 = Release|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Release|x86.ActiveCfg = Release|Any CPU + {B438694F-8FF7-464A-9EC8-10427374471F}.Release|x86.Build.0 = Release|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x64.Build.0 = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x86.Build.0 = Debug|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x64.ActiveCfg = Release|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x64.Build.0 = Release|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.ActiveCfg = Release|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.Build.0 = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.Build.0 = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.Build.0 = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.Build.0 = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.ActiveCfg = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.Build.0 = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.ActiveCfg = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.Build.0 = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.Build.0 = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.Build.0 = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.Build.0 = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.Build.0 = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -90,6 +210,8 @@ Global {F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206} {B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206} {E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {F508E0BA-E885-424F-9D4C-359CF0011DEF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3} diff --git a/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj b/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj index 9e045a89..dd131cf1 100644 --- a/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj +++ b/src/PepperDash.Core.Tests/PepperDash.Core.Tests.csproj @@ -5,6 +5,7 @@ enable false false + false diff --git a/src/PepperDash.Core/Device.cs b/src/PepperDash.Core/Device.cs index ff436b68..e1e9c066 100644 --- a/src/PepperDash.Core/Device.cs +++ b/src/PepperDash.Core/Device.cs @@ -62,7 +62,6 @@ public class Device : IKeyName public Device(string key, string name) : this(key) { Name = name; - } //public Device(DeviceConfig config) diff --git a/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj b/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj index 429afd56..62582959 100644 --- a/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj +++ b/src/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj @@ -5,6 +5,7 @@ enable false false + false From e818c9ca039d98bd2dc462998f18779f59e5c8fb Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 15:24:13 -0600 Subject: [PATCH 035/161] fix: Downgrade setup-dotnet action version to v5 for compatibility --- .github/workflows/EssentialsPlugins-builds-4-series-caller.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml index 660f7294..a8fcfe6a 100644 --- a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml +++ b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v6 + - uses: actions/setup-dotnet@v5 with: dotnet-version: 9.0.x - name: Restore dependencies From daf9b4bda0b34d7b78e5e18204710494457d8576 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 15:47:58 -0600 Subject: [PATCH 036/161] refactor: Change CustomActivate and Initialize methods to protected access in multiple classes for better inheritance control --- PepperDash.Essentials.4Series.sln | 18 ----- docs/docs/technical-docs/Arch-activate.md | 6 +- .../Bridges/BridgeBase.cs | 2 +- .../Comm and IR/ConsoleCommMockDevice.cs | 2 +- .../Crestron/CrestronGenericBaseDevice.cs | 2 +- .../CrestronGenericBaseDevice.cs.orig | 2 +- .../Devices/EssentialsDevice.cs | 2 +- .../Devices/GenericMonitoredTcpDevice.cs | 2 +- .../Fusion/IEssentialsRoomFusionController.cs | 40 +--------- .../MicrophonePrivacyController.cs | 4 +- .../Monitoring/SystemMonitorController.cs | 77 ++++++++++++++++++- .../RoomOnToDefaultSourceWhenOccupied.cs | 2 +- .../Room/EssentialsRoomBase.cs | 2 +- .../Timers/RetriggerableTimer.cs | 2 +- .../Web/EssentialsWebApi.cs | 2 +- .../Displays/ScreenLiftController.cs | 2 +- .../Shades/RelayControlledShade.cs | 2 +- .../Shades/ShadeController.cs | 2 +- .../MobileControlSystemController.cs | 2 +- .../MobileControlSIMPLRoomBridge.cs | 2 +- .../MobileControlTouchpanelController.cs | 2 +- .../MobileControlWebsocketServer.cs | 4 +- 22 files changed, 99 insertions(+), 82 deletions(-) diff --git a/PepperDash.Essentials.4Series.sln b/PepperDash.Essentials.4Series.sln index 58293b87..fc8c20f1 100644 --- a/PepperDash.Essentials.4Series.sln +++ b/PepperDash.Essentials.4Series.sln @@ -164,41 +164,23 @@ Global {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.ActiveCfg = Release|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.Build.0 = Release|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.ActiveCfg = Debug|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.ActiveCfg = Debug|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.Build.0 = Release|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.ActiveCfg = Release|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.Build.0 = Release|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.ActiveCfg = Release|Any CPU - {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.Build.0 = Release|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.ActiveCfg = Debug|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.ActiveCfg = Debug|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.Build.0 = Release|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.Build.0 = Release|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU - {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/docs/technical-docs/Arch-activate.md b/docs/docs/technical-docs/Arch-activate.md index 72a9e3eb..61a0b68b 100644 --- a/docs/docs/technical-docs/Arch-activate.md +++ b/docs/docs/technical-docs/Arch-activate.md @@ -32,7 +32,7 @@ If the `CustomActivate()` method is long, consider breaking it up into many smal Note: It is best-practice in Essentials to not write arbitrarily-timed startup sequences to ensure that a "system" or room is functional. Rather, we encourage the developer to use various properties and conditions on devices to aggregate together "room is ready" statuses that can trigger further action. This ensures that all devices can be up and alive, allowing them to be debugged within a program that may otherwise be misbehaving - as well as not making users and expensive developers wait for code to start up! ```cs -public override bool CustomActivate() +protected override bool CustomActivate() { Debug.Console(0, this, "Final activation. Setting up actions and feedbacks"); SetupFunctions(); @@ -52,7 +52,7 @@ We can see in the example below that during the `CustomActivate()` phase, we def ### **Example** ```cs -public override bool CustomActivate() +protected override bool CustomActivate() { foreach (var i in Config.Inputs) { @@ -115,7 +115,7 @@ The main task that should be undertaken in the `Initialize()` method for any 3rd ### Example (from `PepperDash.Essentials.Devices.Common.VideoCodec.Cisco.CiscoSparkCodec`) ```cs - public override void Initialize() + protected override void Initialize() { var socket = Communication as ISocketStatus; if (socket != null) diff --git a/src/PepperDash.Essentials.Core/Bridges/BridgeBase.cs b/src/PepperDash.Essentials.Core/Bridges/BridgeBase.cs index 4982a642..6adf7d37 100644 --- a/src/PepperDash.Essentials.Core/Bridges/BridgeBase.cs +++ b/src/PepperDash.Essentials.Core/Bridges/BridgeBase.cs @@ -57,7 +57,7 @@ public class EiscApiAdvanced : BridgeApi, ICommunicationMonitor AddPostActivationAction(RegisterEisc); } - public override bool CustomActivate() + protected override bool CustomActivate() { CommunicationMonitor.Start(); return base.CustomActivate(); diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ConsoleCommMockDevice.cs b/src/PepperDash.Essentials.Core/Comm and IR/ConsoleCommMockDevice.cs index 3f7f3097..91a2d76b 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ConsoleCommMockDevice.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ConsoleCommMockDevice.cs @@ -57,7 +57,7 @@ namespace PepperDash.Essentials.Core; /// CustomActivate method /// /// - public override bool CustomActivate() + protected override bool CustomActivate() { Communication.Connect(); CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); }; diff --git a/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs b/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs index afb15015..a8547ba3 100644 --- a/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs +++ b/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs @@ -87,7 +87,7 @@ namespace PepperDash.Essentials.Core; /// CustomActivate method /// /// - public override bool CustomActivate() + protected override bool CustomActivate() { Debug.LogMessage(LogEventLevel.Information, this, "Activating"); if (!PreventRegistration) diff --git a/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs.orig b/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs.orig index bef4ae01..8dc38428 100644 --- a/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs.orig +++ b/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs.orig @@ -36,7 +36,7 @@ namespace PepperDash.Essentials.Core /// Make sure that overriding classes call this! /// Registers the Crestron device, connects up to the base events, starts communication monitor /// - public override bool CustomActivate() + protected override bool CustomActivate() { Debug.LogMessage(LogEventLevel.Information, this, "Activating"); var response = Hardware.RegisterWithLogging(Key); diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs index 3b4f3789..305a14ec 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs @@ -87,7 +87,7 @@ public abstract class EssentialsDevice : Device /// Override this method to perform any initialization that requires all devices to be activated. This method is called automatically after the DeviceManager.AllDevicesActivated event is fired, and should not be called directly. /// /// - public override bool CustomActivate() + protected override bool CustomActivate() { CreateMobileControlMessengers(); diff --git a/src/PepperDash.Essentials.Core/Devices/GenericMonitoredTcpDevice.cs b/src/PepperDash.Essentials.Core/Devices/GenericMonitoredTcpDevice.cs index f43bf8a3..7373f149 100644 --- a/src/PepperDash.Essentials.Core/Devices/GenericMonitoredTcpDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/GenericMonitoredTcpDevice.cs @@ -63,7 +63,7 @@ namespace PepperDash.Essentials.Core.Devices; /// CustomActivate method /// /// - public override bool CustomActivate() + protected override bool CustomActivate() { CommunicationMonitor.Start(); return true; diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index 2da18352..16631a75 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -309,7 +309,7 @@ namespace PepperDash.Essentials.Core.Fusion } /// - public override void Initialize() + protected override void Initialize() { GenerateGuidFile(GetGuidFilePath(_config.IpIdInt)); @@ -501,12 +501,6 @@ namespace PepperDash.Essentials.Core.Fusion // Moved to CurrentRoomSourceNameSig = FusionRoom.CreateOffsetStringSig(JoinMap.Display1CurrentSourceName.JoinNumber, JoinMap.Display1CurrentSourceName.AttributeName, eSigIoMask.InputSigOnly); - // Don't think we need to get current status of this as nothing should be alive yet. - if (Room is IHasCurrentSourceInfoChange hasCurrentSourceInfoChange) - { - hasCurrentSourceInfoChange.CurrentSourceChange += Room_CurrentSourceInfoChange; - } - FusionRoom.SystemPowerOn.OutputSig.SetSigFalseAction(Room.PowerOnToDefaultOrLastSource); FusionRoom.SystemPowerOff.OutputSig.SetSigFalseAction(() => @@ -1749,38 +1743,6 @@ namespace PepperDash.Essentials.Core.Fusion return Convert.ToInt32(capture.Groups[1].Value); } - /// - /// Event handler for when room source changes - /// - protected void Room_CurrentSourceInfoChange(SourceListItem info, ChangeType type) - { - // Handle null. Nothing to do when switching from or to null - if (info == null || info.SourceDevice == null) - { - return; - } - - if (info.SourceDevice is Device dev) - { - if (type == ChangeType.WillChange) - { - if (_sourceToFeedbackSigs.ContainsKey(dev)) - { - _sourceToFeedbackSigs[dev].BoolValue = false; - } - } - else - { - if (_sourceToFeedbackSigs.ContainsKey(dev)) - { - _sourceToFeedbackSigs[dev].BoolValue = true; - } - //var name = (room == null ? "" : room.Name); - CurrentRoomSourceNameSig.InputSig.StringValue = dev.Name; - } - } - } - /// /// Event handler for Fusion state changes /// diff --git a/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs b/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs index 29cd4e3c..8a3facde 100644 --- a/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs +++ b/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs @@ -69,7 +69,7 @@ public class MicrophonePrivacyController : EssentialsDevice Inputs = new List(); } - public override bool CustomActivate() + protected override bool CustomActivate() { foreach (var i in Config.Inputs) { @@ -105,7 +105,7 @@ public class MicrophonePrivacyController : EssentialsDevice #region Overrides of Device - public override void Initialize() + protected override void Initialize() { CheckPrivacyMode(); } diff --git a/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs b/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs index e4784250..295438e7 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs @@ -168,6 +168,20 @@ public class SystemMonitorController : EssentialsBridgeableDevice _uptimePollTimer = null; } + /// + /// Polls the uptime and last start time from the control system and updates the feedbacks + /// This is necessary because there is no event that is fired when these values change, so we have to poll for them + /// at a regular interval + /// Uptime is also polled on activation to get initial values + /// Uptime is polled every 5 minutes (300000 ms) which should be often enough to keep the values updated without causing performance issues + /// Note: polling uptime can cause a delay in the feedback update, so it is done in a separate thread to avoid blocking the main thread + /// Note: this method uses CrestronConsole.SendControlSystemCommand which can be slow, so it is not recommended to call this method more often than every 5 minutes + /// Note: this method will not work on a server as the "uptime" command is not available, but it will not cause any issues either as it will just return an empty string + /// Note: if you need more real-time uptime updates, you could consider implementing a custom solution that tracks uptime internally and updates the feedbacks accordingly, but this would require more complex implementation and testing + /// Note: if you implement a custom solution for tracking uptime, you should still consider polling the uptime from the control system at a regular interval (e.g. every hour) to ensure that your internal tracking is accurate and to account for any potential issues that may arise with your custom implementation + /// Note: if you implement a custom solution for tracking uptime, you should also consider implementing a way to reset the uptime (e.g. on program start) to ensure that it reflects the actual uptime of the control system accurately + /// + /// public void PollUptime(object obj) { var consoleResponse = string.Empty; @@ -306,13 +320,15 @@ public class SystemMonitorController : EssentialsBridgeableDevice } } - public override bool CustomActivate() + /// + protected override bool CustomActivate() { RefreshSystemMonitorData(); return base.CustomActivate(); } + /// public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) { var joinMap = new SystemMonitorJoinMap(joinStart); @@ -675,6 +691,9 @@ public class SystemMonitorController : EssentialsBridgeableDevice CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, adapterIndex)); } + /// + /// Updates all the ethernet status feedbacks for this interface + /// public void UpdateEthernetStatus() { HostNameFeedback.FireUpdate(); @@ -692,26 +711,77 @@ public class SystemMonitorController : EssentialsBridgeableDevice } } + +/// +/// Represents a collection of feedbacks related to the status of a program running on the control system, including its operating state, registration state, and various pieces of information about the program such as its name, compile time, and environment. This class also includes methods for retrieving program information and responding to program change events to keep the feedbacks updated in real-time. +/// public class ProgramStatusFeedbacks { + /// + /// Event that is fired when any of the program information properties change, allowing external classes to respond to changes in program information in real-time + /// public event EventHandler ProgramInfoChanged; + /// + /// Gets or sets the Program associated with this collection of feedbacks + /// public Program Program; + /// + /// Gets or sets the ProgramInfo object that contains detailed information about the program, such as its file name, compile time, environment, and other relevant properties. This object is updated whenever there is a change in the program's operating state or registration state, ensuring that the feedbacks always reflect the most current information about the program. + /// public ProgramInfo ProgramInfo { get; set; } + /// + /// Gets or sets the ProgramStartedFeedback, which is a BoolFeedback that indicates whether the program is currently started (true) or not (false). This feedback is updated in real-time based on the program's operating state, allowing external classes to easily monitor whether the program is running or not. + /// public BoolFeedback ProgramStartedFeedback; + + /// + /// Gets or sets the ProgramStoppedFeedback, which is a BoolFeedback that indicates whether the program is currently stopped (true) or not (false). This feedback is updated in real-time based on the program's operating state, allowing external classes to easily monitor whether the program is stopped or not. + /// public BoolFeedback ProgramStoppedFeedback; + + /// + /// Gets or sets the ProgramRegisteredFeedback, which is a BoolFeedback that indicates whether the program is currently registered (true) or not (false). This feedback is updated in real-time based on the program's registration state, allowing external classes to easily monitor whether the program is registered or not. + /// public BoolFeedback ProgramRegisteredFeedback; + + /// + /// Gets or sets the ProgramUnregisteredFeedback, which is a BoolFeedback that indicates whether the program is currently unregistered (true) or not (false). This feedback is updated in real-time based on the program's registration state, allowing external classes to easily monitor whether the program is unregistered or not. + /// public BoolFeedback ProgramUnregisteredFeedback; + /// + /// Gets or sets the ProgramNameFeedback, which is a StringFeedback that provides the name of the program file. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access the current name of the program file. + /// public StringFeedback ProgramNameFeedback; + + /// + /// Gets or sets the ProgramCompileTimeFeedback, which is a StringFeedback that provides the compile time of the program. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access the current compile time of the program. + /// public StringFeedback ProgramCompileTimeFeedback; + + /// + /// Gets or sets the CrestronDataBaseVersionFeedback, which is a StringFeedback that provides the version of the Crestron database used by the program. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access the current version of the Crestron database used by the program. + /// public StringFeedback CrestronDataBaseVersionFeedback; - // SIMPL windows version + + /// + /// Gets or sets the EnvironmentVersionFeedback, which is a StringFeedback that provides the environment version of the program. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access the current environment version of the program. + /// public StringFeedback EnvironmentVersionFeedback; + + /// + /// Gets or sets the AggregatedProgramInfoFeedback, which is a StringFeedback that provides a JSON serialized string of the entire ProgramInfo object. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access all current information about the program in a single feedback. + /// public StringFeedback AggregatedProgramInfoFeedback; + + /// + /// Constructor for the ProgramStatusFeedbacks class, which initializes all feedbacks based on the provided Program object and retrieves the initial program information to populate the feedbacks with accurate data. This constructor also sets up the necessary event handlers to ensure that the feedbacks are updated in real-time as changes occur to the program's operating state or registration state. + /// + /// public ProgramStatusFeedbacks(Program program) { ProgramInfo = new ProgramInfo(program.Number); @@ -851,6 +921,9 @@ public class ProgramStatusFeedbacks OnProgramInfoChanged(); } + /// + /// Fires the ProgramInfoChanged event to notify external classes that they should update any properties related to program information based on changes to the program's operating state or registration state. This method is called whenever there is a change in the program's information, ensuring that all feedbacks and external classes that rely on program information are always up-to-date with the most current information about the program. + /// public void OnProgramInfoChanged() { //Debug.LogMessage(LogEventLevel.Debug, "Firing ProgramInfoChanged for slot: {0}", Program.Number); diff --git a/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs b/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs index 81b5bf3b..c446bc98 100644 --- a/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs +++ b/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs @@ -75,7 +75,7 @@ public class RoomOnToDefaultSourceWhenOccupied : ReconfigurableDevice }); } - public override bool CustomActivate() + protected override bool CustomActivate() { SetUpDevice(); diff --git a/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs b/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs index e2d35c2a..58e44bb4 100644 --- a/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs +++ b/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs @@ -250,7 +250,7 @@ public abstract class EssentialsRoomBase : ReconfigurableDevice, IEssentialsRoom }); } - public override bool CustomActivate() + protected override bool CustomActivate() { SetUpMobileControl(); diff --git a/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs b/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs index 452517b2..26fd3d72 100644 --- a/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs +++ b/src/PepperDash.Essentials.Core/Timers/RetriggerableTimer.cs @@ -38,7 +38,7 @@ public class RetriggerableTimer : EssentialsDevice } /// - public override bool CustomActivate() + protected override bool CustomActivate() { if (_propertiesConfig.StartTimerOnActivation) { diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index 8fbc6c34..8a66431b 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -208,7 +208,7 @@ public class EssentialsWebApi : EssentialsDevice /// /// Initializes the CWS class /// - public override void Initialize() + protected override void Initialize() { AddRoute(new HttpCwsRoute("apiPaths") { diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index 7f7779c5..a861d6ac 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -182,7 +182,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades /// CustomActivate method /// /// - public override bool CustomActivate() + protected override bool CustomActivate() { //Create ISwitchedOutput objects based on props switch (Mode) diff --git a/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs b/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs index d87d29fb..9e37cd8b 100644 --- a/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs +++ b/src/PepperDash.Essentials.Devices.Common/Shades/RelayControlledShade.cs @@ -45,7 +45,7 @@ public class RelayControlledShade : ShadeBase, IShadesOpenCloseStop } /// - public override bool CustomActivate() + protected override bool CustomActivate() { //Create ISwitchedOutput objects based on props OpenRelay = GetSwitchedOutputFromDevice(Config.Relays.Open); diff --git a/src/PepperDash.Essentials.Devices.Common/Shades/ShadeController.cs b/src/PepperDash.Essentials.Devices.Common/Shades/ShadeController.cs index 7d22ec56..8ccf16cb 100644 --- a/src/PepperDash.Essentials.Devices.Common/Shades/ShadeController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Shades/ShadeController.cs @@ -34,7 +34,7 @@ public class ShadeController : EssentialsDevice, IShades } /// - public override bool CustomActivate() + protected override bool CustomActivate() { foreach (var shadeConfig in Config.Shades) { diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 47bdcfe8..cba10266 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1283,7 +1283,7 @@ namespace PepperDash.Essentials } /// - public override void Initialize() + protected override void Initialize() { if (!Config.EnableMessengerSubscriptions) { diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs index 412507e8..c2b727ce 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs @@ -141,7 +141,7 @@ namespace PepperDash.Essentials.Room.MobileControl /// /// CustomActivate method /// - public override bool CustomActivate() + protected override bool CustomActivate() { this.LogDebug("Final activation. Setting up actions and feedbacks"); //SetupFunctions(); diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs index 30f4542b..24b5e242 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -364,7 +364,7 @@ namespace PepperDash.Essentials.Touchpanel /// /// CustomActivate method /// - public override bool CustomActivate() + protected override bool CustomActivate() { var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this); diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index da357fc5..09b7dec1 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -261,7 +261,7 @@ namespace PepperDash.Essentials.WebSocketServer /// Initialize method /// /// - public override void Initialize() + protected override void Initialize() { try { @@ -276,7 +276,7 @@ namespace PepperDash.Essentials.WebSocketServer { ClientCertificateRequired = false, CheckCertificateRevocation = false, - EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 + EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 }; } From 884b2c2e6e1aa090f02a174e80ccc71de385a429 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 15:49:38 -0600 Subject: [PATCH 037/161] feat: Enhance documentation for MicrophonePrivacyController and its factory with detailed summaries and parameter descriptions --- .../MicrophonePrivacyController.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs b/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs index 8a3facde..fd704f76 100644 --- a/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs +++ b/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs @@ -51,16 +51,31 @@ public class MicrophonePrivacyController : EssentialsDevice bool _enableLeds; + /// + /// Gets or sets the list of digital inputs that are used to toggle the privacy state. Each input is expected to be momentary contact closure that triggers a change in the privacy state when activated. The controller will subscribe to the OutputChange event of each input's InputStateFeedback to monitor for changes in the input state and respond accordingly by toggling the privacy state and updating the LED indicators if enabled. + /// public List Inputs { get; private set; } + /// + /// Gets or sets the GenericRelayDevice that is used to indicate the privacy state with a red LED. When the privacy mode is active, the red LED will be turned on to provide a visual indication of the privacy state. The controller will manage the state of this relay based on changes in the privacy mode, ensuring that it accurately reflects whether the privacy mode is currently active or not. + /// public GenericRelayDevice RedLedRelay { get; private set; } bool _redLedRelayState; + /// + /// Gets or sets the GenericRelayDevice that is used to indicate the privacy state with a green LED. When the privacy mode is inactive, the green LED will be turned on to provide a visual indication of the privacy state. The controller will manage the state of this relay based on changes in the privacy mode, ensuring that it accurately reflects whether the privacy mode is currently active or not. + /// public GenericRelayDevice GreenLedRelay { get; private set; } bool _greenLedRelayState; + /// public IPrivacy PrivacyDevice { get; private set; } + /// + /// Constructor for the MicrophonePrivacyController class, which initializes the controller with the specified key and configuration. The constructor sets up the necessary properties and collections for managing the digital inputs, LED relays, and privacy device. It also prepares the controller for activation by ensuring that all required components are properly initialized and ready to be used when the controller is activated in the system. + /// + /// + /// public MicrophonePrivacyController(string key, MicrophonePrivacyControllerConfig config) : base(key) { @@ -105,6 +120,7 @@ public class MicrophonePrivacyController : EssentialsDevice #region Overrides of Device + /// protected override void Initialize() { CheckPrivacyMode(); @@ -112,6 +128,7 @@ public class MicrophonePrivacyController : EssentialsDevice #endregion + /// public void SetPrivacyDevice(IPrivacy privacyDevice) { PrivacyDevice = privacyDevice; @@ -240,13 +257,21 @@ public class MicrophonePrivacyController : EssentialsDevice } } +/// +/// Factory for creating MicrophonePrivacyController devices +/// public class MicrophonePrivacyControllerFactory : EssentialsDeviceFactory { + /// + /// Constructor for the MicrophonePrivacyControllerFactory class, which initializes the factory and sets up the necessary type names for device creation. This factory is responsible for creating instances of the MicrophonePrivacyController device based on the provided configuration and device key when requested by the system. + /// public MicrophonePrivacyControllerFactory() { TypeNames = new List() { "microphoneprivacycontroller" }; } + + /// public override EssentialsDevice BuildDevice(DeviceConfig dc) { Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MIcrophonePrivacyController Device"); From 7e3c62b303f98b86208e791e03c3a23b3c2532ab Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 15:54:15 -0600 Subject: [PATCH 038/161] fix: Update actions/checkout and actions/setup-dotnet versions for improved compatibility --- ...sentialsPlugins-builds-4-series-caller.yml | 17 +- .github/workflows/essentials-3-dev-build.yml | 247 ------------------ .github/workflows/publish-docs.yml | 4 +- 3 files changed, 4 insertions(+), 264 deletions(-) delete mode 100644 .github/workflows/essentials-3-dev-build.yml diff --git a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml index a8fcfe6a..0733543b 100644 --- a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml +++ b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml @@ -6,22 +6,9 @@ on: - '**' jobs: - # runTests: - # uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-tests.yml@main - # secrets: inherit runTests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 9.0.x - - name: Restore dependencies - working-directory: . - run: dotnet restore - - name: Test - working-directory: . - run: dotnet test --verbosity normal --no-restore --collect:"xplat code coverage" + uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-run-tests.yml@main + secrets: inherit getVersion: uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-getversion.yml@main secrets: inherit diff --git a/.github/workflows/essentials-3-dev-build.yml b/.github/workflows/essentials-3-dev-build.yml deleted file mode 100644 index 3943d5be..00000000 --- a/.github/workflows/essentials-3-dev-build.yml +++ /dev/null @@ -1,247 +0,0 @@ -name: Essentials v3 Development Build - -on: - push: - branches: - - feature-3.0.0/* - - hotfix-3.0.0/* - - release-3.0.0/* - - development-3.0.0 - -env: - SOLUTION_PATH: . - SOLUTION_FILE: PepperDash.Essentials - VERSION: 0.0.0-buildtype-buildnumber - BUILD_TYPE: Debug - RELEASE_BRANCH: main -jobs: - Build_Project_4-Series: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - # Detect environment (Act vs GitHub) - - name: Detect environment - id: detect_env - run: | - if [ -n "$ACT" ]; then - echo "is_local=true" >> $GITHUB_OUTPUT - else - echo "is_local=false" >> $GITHUB_OUTPUT - fi - - - name: Install prerequisites - run: | - if [ "${{ steps.detect_env.outputs.is_local }}" == "true" ]; then - # For Act - no sudo needed - apt-get update - apt-get install -y curl wget libicu-dev git unzip - else - # For GitHub runners - sudo required - sudo apt-get update - sudo apt-get install -y curl wget libicu-dev git unzip - fi - - - name: Set Version Number - id: setVersion - shell: bash - run: | - latestVersion="3.0.0" - newVersion=$latestVersion - phase="" - newVersionString="" - - if [[ $GITHUB_REF =~ ^refs/pull/.* ]]; then - phase="beta" - newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}" - elif [[ $GITHUB_REF =~ ^refs/heads/hotfix-3.0.0/.* ]]; then - phase="hotfix" - newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}" - elif [[ $GITHUB_REF =~ ^refs/heads/feature-3.0.0/.* ]]; then - phase="alpha" - newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}" - elif [[ $GITHUB_REF == "refs/heads/development-3.0.0" ]]; then - phase="beta" - newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}" - elif [[ $GITHUB_REF =~ ^refs/heads/release-3.0.0/.* ]]; then - version=$(echo $GITHUB_REF | awk -F '/' '{print $NF}' | sed 's/v//') - phase="rc" - newVersionString="${version}-${phase}-${GITHUB_RUN_NUMBER}" - else - # For local builds or unrecognized branches - newVersionString="${newVersion}-local" - fi - - echo "version=$newVersionString" >> $GITHUB_OUTPUT - - # Create Build Properties file - - name: Create Build Properties - run: | - cat > Directory.Build.props << EOF - - - ${{ steps.setVersion.outputs.version }} - ${{ steps.setVersion.outputs.version }} - ${{ steps.setVersion.outputs.version }} - ${{ steps.setVersion.outputs.version }} - ${{ steps.setVersion.outputs.version }} - ${{ steps.setVersion.outputs.version }} - - - EOF - - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '8.0.x' - - - name: Restore NuGet Packages - run: dotnet restore ${SOLUTION_FILE}.sln - - - name: Build Solution - run: dotnet build ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --no-restore - - # Copy the CPZ file to the output directory with version in the filename - - name: Copy and Rename CPZ Files - run: | - mkdir -p ./output/cpz - - # Find the main CPZ file in the build output - if [ -f "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" ]; then - cp "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" "./output/cpz/PepperDashEssentials.${{ steps.setVersion.outputs.version }}.cpz" - echo "Main CPZ file copied and renamed successfully." - else - echo "Warning: Main CPZ file not found at expected location." - find ./src -name "*.cpz" | xargs -I {} cp {} ./output/cpz/ - fi - - - name: Pack Solution - run: dotnet pack ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --output ./output/nuget --no-build - - # List build artifacts (runs in both environments) - - name: List Build Artifacts - run: | - echo "=== Build Artifacts ===" - echo "NuGet Packages:" - find ./output/nuget -type f | sort - echo "" - echo "CPZ/CPLZ Files:" - find ./output -name "*.cpz" -o -name "*.cplz" | sort - echo "=======================" - - # Enhanced package inspection for local runs - - name: Inspect NuGet Packages - if: steps.detect_env.outputs.is_local == 'true' - run: | - echo "=== NuGet Package Details ===" - for pkg in $(find ./output/nuget -name "*.nupkg"); do - echo "Package: $(basename "$pkg")" - echo "Size: $(du -h "$pkg" | cut -f1)" - - # Extract and show package contents - echo "Contents:" - unzip -l "$pkg" | tail -n +4 | head -n -2 - echo "--------------------------" - - # Try to extract and show the nuspec file (contains metadata) - echo "Metadata:" - unzip -p "$pkg" "*.nuspec" 2>/dev/null | grep -E "(||||)" || echo "Metadata extraction failed" - echo "--------------------------" - done - echo "===========================" - - # Tag creation - GitHub version - - name: Create tag for non-rc builds (GitHub) - if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'false' }} - run: | - git config --global user.name "GitHub Actions" - git config --global user.email "actions@github.com" - git tag ${{ steps.setVersion.outputs.version }} - git push --tags origin - - # Tag creation - Act mock version - - name: Create tag for non-rc builds (Act Mock) - if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'true' }} - run: | - echo "Would create git tag: ${{ steps.setVersion.outputs.version }}" - echo "Would push tag to: origin" - - # Release creation - GitHub version - - name: Create Release (GitHub) - if: steps.detect_env.outputs.is_local == 'false' - id: create_release - uses: ncipollo/release-action@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - artifacts: 'output/cpz/*,output/**/*.cplz' - generateReleaseNotes: true - prerelease: ${{contains('debug', env.BUILD_TYPE)}} - tag: ${{ steps.setVersion.outputs.version }} - - # Release creation - Act mock version with enhanced output - - name: Create Release (Act Mock) - if: steps.detect_env.outputs.is_local == 'true' - run: | - echo "=== Mock Release Creation ===" - echo "Would create release with:" - echo "- Tag: ${{ steps.setVersion.outputs.version }}" - echo "- Prerelease: ${{contains('debug', env.BUILD_TYPE)}}" - echo "- Artifacts matching pattern: output/cpz/*,output/**/*.cplz" - echo "" - echo "Matching artifacts:" - find ./output/cpz -type f - find ./output -name "*.cplz" - - # Detailed info about release artifacts - echo "" - echo "Artifact Details:" - for artifact in $(find ./output/cpz -type f; find ./output -name "*.cplz"); do - echo "File: $(basename "$artifact")" - echo "Size: $(du -h "$artifact" | cut -f1)" - echo "Created: $(stat -c %y "$artifact")" - echo "MD5: $(md5sum "$artifact" | cut -d' ' -f1)" - echo "--------------------------" - done - echo "============================" - - # NuGet setup - GitHub version - - name: Setup NuGet (GitHub) - if: steps.detect_env.outputs.is_local == 'false' - run: | - dotnet nuget add source https://nuget.pkg.github.com/pepperdash/index.json -n github -u pepperdash -p ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text - - # NuGet setup - Act mock version - - name: Setup NuGet (Act Mock) - if: steps.detect_env.outputs.is_local == 'true' - run: | - echo "=== Mock NuGet Setup ===" - echo "Would add GitHub NuGet source: https://nuget.pkg.github.com/pepperdash/index.json" - echo "=======================" - - # Publish to NuGet - GitHub version - - name: Publish to Nuget (GitHub) - if: steps.detect_env.outputs.is_local == 'false' - run: dotnet nuget push ./output/nuget/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} - - # Publish to NuGet - Act mock version - - name: Publish to Nuget (Act Mock) - if: steps.detect_env.outputs.is_local == 'true' - run: | - echo "=== Mock Publish to NuGet ===" - echo "Would publish the following packages to https://api.nuget.org/v3/index.json:" - find ./output/nuget -name "*.nupkg" | sort - echo "=============================" - - # Publish to GitHub NuGet - GitHub version - - name: Publish to Github Nuget (GitHub) - if: steps.detect_env.outputs.is_local == 'false' - run: dotnet nuget push ./output/nuget/*.nupkg --source github --api-key ${{ secrets.GITHUB_TOKEN }} - - # Publish to GitHub NuGet - Act mock version - - name: Publish to Github Nuget (Act Mock) - if: steps.detect_env.outputs.is_local == 'true' - run: | - echo "=== Mock Publish to GitHub NuGet ===" - echo "Would publish the following packages to the GitHub NuGet registry:" - find ./output/nuget -name "*.nupkg" | sort - echo "==================================" \ No newline at end of file diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 42dd64c5..35752ff7 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -26,9 +26,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Dotnet Setup - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 8.x From fa9dc3a2b1c4a7f1aa6091dfa9150e61f1e12f06 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 16:46:45 -0600 Subject: [PATCH 039/161] feat: Add devToolsVersion to build configuration for enhanced version control --- .github/workflows/EssentialsPlugins-builds-4-series-caller.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml index 0733543b..121e6cf9 100644 --- a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml +++ b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml @@ -23,4 +23,5 @@ jobs: version: ${{ needs.getVersion.outputs.version }} tag: ${{ needs.getVersion.outputs.tag }} channel: ${{ needs.getVersion.outputs.channel }} - bypassPackageCheck: true \ No newline at end of file + bypassPackageCheck: true + devToolsVersion: ${{ vars.ESSENTIALSDEVTOOLSVERSION }} \ No newline at end of file From 35d44f91e47a44644786a4d441b78fd3e720a51d Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 16:53:29 -0600 Subject: [PATCH 040/161] feat: Implement devTools zip file extraction and cleanup process for improved debugging support --- src/PepperDash.Essentials/ControlSystem.cs | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index ffb8a2d3..275b827e 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -632,6 +632,65 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig File.Delete(file.FullName); } + var devToolsZipFiles = applicationDirectory.GetFiles("essentials-devtools*.zip"); + + if (devToolsZipFiles.Length > 1) + { + throw new Exception("Multiple essentials-devtools zip files found in application directory. Please ensure only one essentials-devtools*.zip file is present and retry."); + } + + if (devToolsZipFiles.Length == 1) + { + var devToolsZipFile = devToolsZipFiles[0]; + var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + var userOrNvramDir = programDir.Parent; + var rootDir = userOrNvramDir?.Parent; + if (rootDir == null) + { + throw new Exception($"Unable to determine root directory for debug html extraction. Current path: {Global.FilePathPrefix}"); + } + var debugDir = Path.Combine(Path.Combine(rootDir.FullName, "html"), "debug"); + var debugRoot = System.IO.Path.GetFullPath(debugDir); + if (!debugRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && + !debugRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + debugRoot += Path.DirectorySeparatorChar; + } + Debug.LogMessage(LogEventLevel.Information, "Found essentials-devtools zip file: {zipFile:l}... Unzipping to {Destination}...", devToolsZipFile.FullName, debugDir); + using (var archive = ZipFile.OpenRead(devToolsZipFile.FullName)) + { + foreach (var entry in archive.Entries) + { + var destinationPath = Path.Combine(debugDir, entry.FullName); + var fullDest = System.IO.Path.GetFullPath(destinationPath); + if (!fullDest.StartsWith(debugRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(destinationPath); + continue; + } + + if (File.Exists(destinationPath)) + File.Delete(destinationPath); + + var parentDir = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(parentDir)) + Directory.CreateDirectory(parentDir); + + entry.ExtractToFile(destinationPath, true); + Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); + } + } + } + + // cleaning up devtools zip files + foreach (var file in devToolsZipFiles) + { + File.Delete(file.FullName); + } + var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json"); if (jsonFiles.Length > 1) From 4ed5e648c0c13241bf53854a876d122becbda87b Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Apr 2026 17:00:16 -0600 Subject: [PATCH 041/161] feat: Load assets during ControlSystem initialization for enhanced resource management --- src/PepperDash.Essentials/ControlSystem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 275b827e..c91f8927 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -271,6 +271,8 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig _ = new Core.DeviceFactory(); + LoadAssets(); + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); var filesReady = SetupFilesystem(); From b85f1dab6b25b00ebe4a0d0bf9bae437677b52a7 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 9 Apr 2026 14:00:27 -0600 Subject: [PATCH 042/161] feat: Add asset loading functionality and related tests for improved resource management --- PepperDash.Essentials.4Series.sln | 21 ++ .../ControlSystem/LoadAssetsTests.cs | 334 ++++++++++++++++++ .../Fakes/Fakes.cs | 61 ++++ .../PepperDash.Essentials.Tests.csproj | 25 ++ .../TestInitializer.cs | 28 ++ src/PepperDash.Essentials/AssetLoader.cs | 256 ++++++++++++++ src/PepperDash.Essentials/ControlSystem.cs | 200 +---------- .../PepperDash.Essentials.csproj | 5 + 8 files changed, 733 insertions(+), 197 deletions(-) create mode 100644 src/PepperDash.Essentials.Tests/ControlSystem/LoadAssetsTests.cs create mode 100644 src/PepperDash.Essentials.Tests/Fakes/Fakes.cs create mode 100644 src/PepperDash.Essentials.Tests/PepperDash.Essentials.Tests.csproj create mode 100644 src/PepperDash.Essentials.Tests/TestInitializer.cs create mode 100644 src/PepperDash.Essentials/AssetLoader.cs diff --git a/PepperDash.Essentials.4Series.sln b/PepperDash.Essentials.4Series.sln index fc8c20f1..a39555e8 100644 --- a/PepperDash.Essentials.4Series.sln +++ b/PepperDash.Essentials.4Series.sln @@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core.Tests", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Core.Tests", "src\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{F508E0BA-E885-424F-9D4C-359CF0011DEF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Tests", "src\PepperDash.Essentials.Tests\PepperDash.Essentials.Tests.csproj", "{841EE676-7784-4456-8F76-3697C6D432A6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU @@ -181,6 +183,24 @@ Global {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x64.ActiveCfg = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x64.Build.0 = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x86.ActiveCfg = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x86.Build.0 = Debug|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Release|Any CPU.Build.0 = Release|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Release|x64.ActiveCfg = Release|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Release|x64.Build.0 = Release|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Release|x86.ActiveCfg = Release|Any CPU + {841EE676-7784-4456-8F76-3697C6D432A6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -194,6 +214,7 @@ Global {E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {680BA287-E61F-4B8D-BD7A-84C2504F5F9C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {F508E0BA-E885-424F-9D4C-359CF0011DEF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {841EE676-7784-4456-8F76-3697C6D432A6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3} diff --git a/src/PepperDash.Essentials.Tests/ControlSystem/LoadAssetsTests.cs b/src/PepperDash.Essentials.Tests/ControlSystem/LoadAssetsTests.cs new file mode 100644 index 00000000..0d06bcb8 --- /dev/null +++ b/src/PepperDash.Essentials.Tests/ControlSystem/LoadAssetsTests.cs @@ -0,0 +1,334 @@ +using System.IO.Compression; +using FluentAssertions; +using Xunit; + +namespace PepperDash.Essentials.Tests.ControlSystem; + +/// +/// Tests for . +/// is the System.IO-based implementation that backs +/// . Tests run against a +/// temporary directory tree so no Crestron runtime is required. +/// Debug is initialised with fakes via TestInitializer. +/// +public sealed class LoadAssetsTests : IDisposable +{ + // --------------------------------------------------------------------------- + // Fixture: each test gets an isolated temp directory tree + // + // _rootDir/ + // appdir/ ← applicationDirectoryPath (where the loader scans) + // user/ + // program1/ ← filePathPrefix (where assets land) + // --------------------------------------------------------------------------- + + private readonly string _rootDir; + private readonly string _appDir; + private readonly string _filePathPrefix; + + public LoadAssetsTests() + { + _rootDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + _appDir = Path.Combine(_rootDir, "appdir"); + _filePathPrefix = Path.Combine(_rootDir, "user", "program1") + Path.DirectorySeparatorChar; + + Directory.CreateDirectory(_appDir); + Directory.CreateDirectory(_filePathPrefix); + } + + public void Dispose() + { + if (Directory.Exists(_rootDir)) + Directory.Delete(_rootDir, recursive: true); + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private static byte[] EmptyZip() + { + using var ms = new MemoryStream(); + using (var _ = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) { } + return ms.ToArray(); + } + + private static byte[] ZipWithFile(string entryName, string content = "test") + { + using var ms = new MemoryStream(); + using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + var entry = archive.CreateEntry(entryName); + using var sw = new StreamWriter(entry.Open()); + sw.Write(content); + } + return ms.ToArray(); + } + + private static byte[] ZipWithDirectory(string directoryEntryName) + { + // Directory entries have a trailing slash and an empty Name + var normalised = directoryEntryName.TrimEnd('/') + '/'; + using var ms = new MemoryStream(); + using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + archive.CreateEntry(normalised); + } + return ms.ToArray(); + } + + private static byte[] ZipWithTraversalEntry() + { + using var ms = new MemoryStream(); + using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + var entry = archive.CreateEntry("../traversal.txt"); + using var sw = new StreamWriter(entry.Open()); + sw.Write("should not appear"); + } + return ms.ToArray(); + } + + private void WriteToAppDir(string fileName, byte[] contents) => + File.WriteAllBytes(Path.Combine(_appDir, fileName), contents); + + private void WriteTextToAppDir(string fileName, string text) => + File.WriteAllText(Path.Combine(_appDir, fileName), text); + + // --------------------------------------------------------------------------- + // No-op cases — nothing in the application directory + // --------------------------------------------------------------------------- + + [Fact] + public void LoadAssets_EmptyApplicationDirectory_DoesNotThrow() + { + var act = () => AssetLoader.Load(_appDir, _filePathPrefix); + act.Should().NotThrow(); + } + + // --------------------------------------------------------------------------- + // assets*.zip + // --------------------------------------------------------------------------- + + [Fact] + public void LoadAssets_MultipleAssetsZips_ThrowsException() + { + WriteToAppDir("assets1.zip", EmptyZip()); + WriteToAppDir("assets2.zip", EmptyZip()); + + var act = () => AssetLoader.Load(_appDir, _filePathPrefix); + act.Should().Throw().WithMessage("*Multiple assets zip files*"); + } + + [Fact] + public void LoadAssets_SingleAssetsZip_ExtractsFileToFilePathPrefix() + { + WriteToAppDir("assets.zip", ZipWithFile("config.json", "{}")); + + AssetLoader.Load(_appDir, _filePathPrefix); + + File.Exists(Path.Combine(_filePathPrefix, "config.json")).Should().BeTrue(); + } + + [Fact] + public void LoadAssets_SingleAssetsZip_FileContentsArePreserved() + { + const string expected = "{\"key\":\"value\"}"; + WriteToAppDir("assets.zip", ZipWithFile("data.json", expected)); + + AssetLoader.Load(_appDir, _filePathPrefix); + + File.ReadAllText(Path.Combine(_filePathPrefix, "data.json")).Should().Be(expected); + } + + [Fact] + public void LoadAssets_SingleAssetsZip_ZipIsDeletedAfterExtraction() + { + var zipPath = Path.Combine(_appDir, "assets.zip"); + WriteToAppDir("assets.zip", ZipWithFile("file.txt")); + + AssetLoader.Load(_appDir, _filePathPrefix); + + File.Exists(zipPath).Should().BeFalse("assets zip should be cleaned up after extraction"); + } + + [Fact] + public void LoadAssets_AssetsZipWithDirectoryEntry_CreatesDirectory() + { + WriteToAppDir("assets.zip", ZipWithDirectory("subdir/")); + + AssetLoader.Load(_appDir, _filePathPrefix); + + Directory.Exists(Path.Combine(_filePathPrefix, "subdir/")).Should().BeTrue(); + } + + [Fact] + public void LoadAssets_AssetsZipWithPathTraversal_ThrowsInvalidOperationException() + { + WriteToAppDir("assets.zip", ZipWithTraversalEntry()); + + var act = () => AssetLoader.Load(_appDir, _filePathPrefix); + act.Should().Throw() + .WithMessage("*trying to extract outside of the target directory*"); + } + + // --------------------------------------------------------------------------- + // htmlassets*.zip + // --------------------------------------------------------------------------- + + [Fact] + public void LoadAssets_MultipleHtmlAssetsZips_ThrowsException() + { + WriteToAppDir("htmlassets1.zip", EmptyZip()); + WriteToAppDir("htmlassets2.zip", EmptyZip()); + + var act = () => AssetLoader.Load(_appDir, _filePathPrefix); + act.Should().Throw().WithMessage("*Multiple htmlassets zip files*"); + } + + [Fact] + public void LoadAssets_SingleHtmlAssetsZip_ExtractsToHtmlDirectory() + { + // htmlDir = rootDir/html + WriteToAppDir("htmlassets.zip", ZipWithFile("index.html", "")); + + AssetLoader.Load(_appDir, _filePathPrefix); + + var expectedHtmlDir = Path.Combine(_rootDir, "html"); + File.Exists(Path.Combine(expectedHtmlDir, "index.html")).Should().BeTrue(); + } + + [Fact] + public void LoadAssets_SingleHtmlAssetsZip_ZipIsDeletedAfterExtraction() + { + var zipPath = Path.Combine(_appDir, "htmlassets.zip"); + WriteToAppDir("htmlassets.zip", ZipWithFile("page.html")); + + AssetLoader.Load(_appDir, _filePathPrefix); + + File.Exists(zipPath).Should().BeFalse(); + } + + [Fact] + public void LoadAssets_HtmlAssetsZipWithPathTraversal_ThrowsInvalidOperationException() + { + WriteToAppDir("htmlassets.zip", ZipWithTraversalEntry()); + + var act = () => AssetLoader.Load(_appDir, _filePathPrefix); + act.Should().Throw() + .WithMessage("*trying to extract outside of the target directory*"); + } + + [Fact] + public void LoadAssets_HtmlAssetsZip_ShallowFilePathPrefixThrowsWhenRootCannotBeDetermined() + { + // A path that is exactly ONE level below the filesystem root has no grandparent, + // so rootDir (programDir.Parent.Parent) is null and the guard should throw. + // DirectoryInfo works with non-existent paths, so we don't create the directory. + var filesystemRoot = Path.GetPathRoot(Path.GetTempPath())!; + var shallowPrefix = Path.Combine(filesystemRoot, "pepperDashTestShallow") + Path.DirectorySeparatorChar; + WriteToAppDir("htmlassets.zip", ZipWithFile("page.html")); + + var act = () => AssetLoader.Load(_appDir, shallowPrefix); + act.Should().Throw().WithMessage("*Unable to determine root directory for html extraction*"); + } + + // --------------------------------------------------------------------------- + // essentials-devtools*.zip + // --------------------------------------------------------------------------- + + [Fact] + public void LoadAssets_MultipleDevToolsZips_ThrowsException() + { + WriteToAppDir("essentials-devtools1.zip", EmptyZip()); + WriteToAppDir("essentials-devtools2.zip", EmptyZip()); + + var act = () => AssetLoader.Load(_appDir, _filePathPrefix); + act.Should().Throw().WithMessage("*Multiple essentials-devtools zip files*"); + } + + [Fact] + public void LoadAssets_SingleDevToolsZip_ExtractsToHtmlDebugDirectory() + { + // debugDir = rootDir/html/debug + WriteToAppDir("essentials-devtools.zip", ZipWithFile("app.js", "console.log('hi');")); + + AssetLoader.Load(_appDir, _filePathPrefix); + + var expectedDebugDir = Path.Combine(_rootDir, "html", "debug"); + File.Exists(Path.Combine(expectedDebugDir, "app.js")).Should().BeTrue(); + } + + [Fact] + public void LoadAssets_SingleDevToolsZip_ZipIsDeletedAfterExtraction() + { + var zipPath = Path.Combine(_appDir, "essentials-devtools.zip"); + WriteToAppDir("essentials-devtools.zip", ZipWithFile("tool.js")); + + AssetLoader.Load(_appDir, _filePathPrefix); + + File.Exists(zipPath).Should().BeFalse(); + } + + [Fact] + public void LoadAssets_DevToolsZipWithPathTraversal_ThrowsInvalidOperationException() + { + WriteToAppDir("essentials-devtools.zip", ZipWithTraversalEntry()); + + var act = () => AssetLoader.Load(_appDir, _filePathPrefix); + act.Should().Throw() + .WithMessage("*trying to extract outside of the target directory*"); + } + + // --------------------------------------------------------------------------- + // *configurationFile*.json + // --------------------------------------------------------------------------- + + [Fact] + public void LoadAssets_MultipleJsonConfigFiles_ThrowsException() + { + WriteTextToAppDir("abcconfigurationFile1.json", "{}"); + WriteTextToAppDir("abcconfigurationFile2.json", "{}"); + + var act = () => AssetLoader.Load(_appDir, _filePathPrefix); + act.Should().Throw().WithMessage("*Multiple configuration files found*"); + } + + [Fact] + public void LoadAssets_SingleJsonConfigFile_IsMovedToFilePathPrefix() + { + const string fileName = "myconfigurationFile.json"; + WriteTextToAppDir(fileName, "{}"); + + AssetLoader.Load(_appDir, _filePathPrefix); + + File.Exists(Path.Combine(_appDir, fileName)).Should().BeFalse("source file should be moved, not copied"); + File.Exists(Path.Combine(_filePathPrefix, fileName)).Should().BeTrue("file should exist at the file path prefix"); + } + + [Fact] + public void LoadAssets_SingleJsonConfigFile_ExistingDestinationIsReplaced() + { + const string fileName = "myconfigurationFile.json"; + WriteTextToAppDir(fileName, "new content"); + + // Pre-populate the destination with stale content + File.WriteAllText(Path.Combine(_filePathPrefix, fileName), "old content"); + + AssetLoader.Load(_appDir, _filePathPrefix); + + File.ReadAllText(Path.Combine(_filePathPrefix, fileName)).Should().Be("new content"); + } + + [Fact] + public void LoadAssets_SingleJsonConfigFile_ContentIsPreserved() + { + const string content = "{\"devices\":[]}"; + const string fileName = "myconfigurationFile.json"; + WriteTextToAppDir(fileName, content); + + AssetLoader.Load(_appDir, _filePathPrefix); + + File.ReadAllText(Path.Combine(_filePathPrefix, fileName)).Should().Be(content); + } +} diff --git a/src/PepperDash.Essentials.Tests/Fakes/Fakes.cs b/src/PepperDash.Essentials.Tests/Fakes/Fakes.cs new file mode 100644 index 00000000..ba195788 --- /dev/null +++ b/src/PepperDash.Essentials.Tests/Fakes/Fakes.cs @@ -0,0 +1,61 @@ +using PepperDash.Core.Abstractions; + +namespace PepperDash.Essentials.Tests.Fakes; + +internal class FakeCrestronEnvironment : ICrestronEnvironment +{ + public DevicePlatform DevicePlatform { get; set; } = DevicePlatform.Appliance; + public RuntimeEnvironment RuntimeEnvironment { get; set; } = RuntimeEnvironment.SimplSharpPro; + public string NewLine { get; set; } = "\r\n"; + public uint ApplicationNumber { get; set; } = 1; + public uint RoomId { get; set; } = 0; + + public event EventHandler? ProgramStatusChanged + { + add { } + remove { } + } + + public event EventHandler? EthernetEventReceived + { + add { } + remove { } + } + + public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath(); + public bool IsHardwareRuntime => false; +} + +internal class NoOpCrestronConsole : ICrestronConsole +{ + public void PrintLine(string message) { } + public void Print(string message) { } + public void ConsoleCommandResponse(string message) { } + public void AddNewConsoleCommand(Action _, string __, string ___, ConsoleAccessLevel ____) { } +} + +internal class InMemoryCrestronDataStore : ICrestronDataStore +{ + private readonly Dictionary _store = new(); + + public void InitStore() { } + + public bool TryGetLocalInt(string key, out int value) + { + if (_store.TryGetValue(key, out var raw) && raw is int i) { value = i; return true; } + value = 0; + return false; + } + + public bool SetLocalInt(string key, int value) { _store[key] = value; return true; } + public bool SetLocalUint(string key, uint value) { _store[key] = (int)value; return true; } + + public bool TryGetLocalBool(string key, out bool value) + { + if (_store.TryGetValue(key, out var raw) && raw is bool b) { value = b; return true; } + value = false; + return false; + } + + public bool SetLocalBool(string key, bool value) { _store[key] = value; return true; } +} diff --git a/src/PepperDash.Essentials.Tests/PepperDash.Essentials.Tests.csproj b/src/PepperDash.Essentials.Tests/PepperDash.Essentials.Tests.csproj new file mode 100644 index 00000000..b32ff955 --- /dev/null +++ b/src/PepperDash.Essentials.Tests/PepperDash.Essentials.Tests.csproj @@ -0,0 +1,25 @@ + + + net9.0 + enable + enable + false + false + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/PepperDash.Essentials.Tests/TestInitializer.cs b/src/PepperDash.Essentials.Tests/TestInitializer.cs new file mode 100644 index 00000000..acf5e271 --- /dev/null +++ b/src/PepperDash.Essentials.Tests/TestInitializer.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; +using PepperDash.Core.Abstractions; +using PepperDash.Essentials.Tests.Fakes; + +namespace PepperDash.Essentials.Tests; + +/// +/// Runs once before any type in this assembly is accessed. +/// Registers fake Crestron service implementations so that the Debug static +/// constructor never tries to reach the real Crestron SDK. +/// +internal static class TestInitializer +{ + [ModuleInitializer] + internal static void Initialize() + { + DebugServiceRegistration.Register( + new FakeCrestronEnvironment + { + DevicePlatform = DevicePlatform.Server, + RuntimeEnvironment = RuntimeEnvironment.Other, + }, + new NoOpCrestronConsole(), + new InMemoryCrestronDataStore()); + } +} + + diff --git a/src/PepperDash.Essentials/AssetLoader.cs b/src/PepperDash.Essentials/AssetLoader.cs new file mode 100644 index 00000000..9db98c7e --- /dev/null +++ b/src/PepperDash.Essentials/AssetLoader.cs @@ -0,0 +1,256 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using PepperDash.Core; +using Serilog.Events; + +namespace PepperDash.Essentials; + +/// +/// Handles extracting embedded asset bundles and moving configuration files from the +/// application directory to the program file-path prefix at startup. +/// Implemented using System.IO types so it can run (and be tested) outside +/// of a Crestron runtime environment. +/// +internal static class AssetLoader +{ + /// + /// Scans for well-known zip bundles and + /// JSON configuration files and deploys them to . + /// + /// + /// The directory to scan (typically the Crestron application root). + /// + /// + /// The program's runtime working directory (e.g. /nvram/program1/). + /// + internal static void Load(string applicationDirectoryPath, string filePathPrefix) + { + var applicationDirectory = new DirectoryInfo(applicationDirectoryPath); + Debug.LogMessage(LogEventLevel.Information, + "Searching: {applicationDirectory:l} for embedded assets - {Destination}", + applicationDirectory.FullName, filePathPrefix); + + ExtractAssetsZip(applicationDirectory, filePathPrefix); + ExtractHtmlAssetsZip(applicationDirectory, filePathPrefix); + ExtractDevToolsZip(applicationDirectory, filePathPrefix); + MoveConfigurationFile(applicationDirectory, filePathPrefix); + } + + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + + private static void ExtractAssetsZip(DirectoryInfo applicationDirectory, string filePathPrefix) + { + var zipFiles = applicationDirectory.GetFiles("assets*.zip"); + + if (zipFiles.Length > 1) + throw new Exception("Multiple assets zip files found. Cannot continue."); + + if (zipFiles.Length == 1) + { + var zipFile = zipFiles[0]; + var assetsRoot = Path.GetFullPath(filePathPrefix); + if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && + !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + assetsRoot += Path.DirectorySeparatorChar; + } + + Debug.LogMessage(LogEventLevel.Information, + "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName); + + using (var archive = ZipFile.OpenRead(zipFile.FullName)) + { + foreach (var entry in archive.Entries) + { + var destinationPath = Path.Combine(filePathPrefix, entry.FullName); + var fullDest = Path.GetFullPath(destinationPath); + if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException( + $"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(destinationPath); + continue; + } + + if (Directory.Exists(destinationPath)) + Directory.Delete(destinationPath, recursive: true); + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); + Debug.LogMessage(LogEventLevel.Information, + "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); + } + } + } + + foreach (var file in zipFiles) + File.Delete(file.FullName); + } + + private static void ExtractHtmlAssetsZip(DirectoryInfo applicationDirectory, string filePathPrefix) + { + var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip"); + + if (htmlZipFiles.Length > 1) + throw new Exception( + "Multiple htmlassets zip files found in application directory. " + + "Please ensure only one htmlassets*.zip file is present and retry."); + + if (htmlZipFiles.Length == 1) + { + var htmlZipFile = htmlZipFiles[0]; + var programDir = new DirectoryInfo( + filePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + var userOrNvramDir = programDir.Parent; + var rootDir = userOrNvramDir?.Parent; + if (rootDir == null) + throw new Exception( + $"Unable to determine root directory for html extraction. Current path: {filePathPrefix}"); + + var htmlDir = Path.Combine(rootDir.FullName, "html"); + var htmlRoot = Path.GetFullPath(htmlDir); + if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && + !htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + htmlRoot += Path.DirectorySeparatorChar; + } + + Debug.LogMessage(LogEventLevel.Information, + "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName); + + using (var archive = ZipFile.OpenRead(htmlZipFile.FullName)) + { + foreach (var entry in archive.Entries) + { + var destinationPath = Path.Combine(htmlDir, entry.FullName); + var fullDest = Path.GetFullPath(destinationPath); + if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException( + $"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(destinationPath); + continue; + } + + if (File.Exists(destinationPath)) + File.Delete(destinationPath); + + var parentDir = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(parentDir)) + Directory.CreateDirectory(parentDir); + + entry.ExtractToFile(destinationPath, overwrite: true); + Debug.LogMessage(LogEventLevel.Information, + "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); + } + } + } + + foreach (var file in htmlZipFiles) + File.Delete(file.FullName); + } + + private static void ExtractDevToolsZip(DirectoryInfo applicationDirectory, string filePathPrefix) + { + var devToolsZipFiles = applicationDirectory.GetFiles("essentials-devtools*.zip"); + + if (devToolsZipFiles.Length > 1) + throw new Exception( + "Multiple essentials-devtools zip files found in application directory. " + + "Please ensure only one essentials-devtools*.zip file is present and retry."); + + if (devToolsZipFiles.Length == 1) + { + var devToolsZipFile = devToolsZipFiles[0]; + var programDir = new DirectoryInfo( + filePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + var userOrNvramDir = programDir.Parent; + var rootDir = userOrNvramDir?.Parent; + if (rootDir == null) + throw new Exception( + $"Unable to determine root directory for debug html extraction. Current path: {filePathPrefix}"); + + var debugDir = Path.Combine(rootDir.FullName, "html", "debug"); + var debugRoot = Path.GetFullPath(debugDir); + if (!debugRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && + !debugRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + debugRoot += Path.DirectorySeparatorChar; + } + + Debug.LogMessage(LogEventLevel.Information, + "Found essentials-devtools zip file: {zipFile:l}... Unzipping to {Destination}...", + devToolsZipFile.FullName, debugDir); + + using (var archive = ZipFile.OpenRead(devToolsZipFile.FullName)) + { + foreach (var entry in archive.Entries) + { + var destinationPath = Path.Combine(debugDir, entry.FullName); + var fullDest = Path.GetFullPath(destinationPath); + if (!fullDest.StartsWith(debugRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException( + $"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(destinationPath); + continue; + } + + if (File.Exists(destinationPath)) + File.Delete(destinationPath); + + var parentDir = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(parentDir)) + Directory.CreateDirectory(parentDir); + + entry.ExtractToFile(destinationPath, overwrite: true); + Debug.LogMessage(LogEventLevel.Information, + "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); + } + } + } + + foreach (var file in devToolsZipFiles) + File.Delete(file.FullName); + } + + private static void MoveConfigurationFile(DirectoryInfo applicationDirectory, string filePathPrefix) + { + var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json"); + + if (jsonFiles.Length > 1) + { + Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", + jsonFiles.Select(f => f.FullName).ToArray()); + throw new Exception("Multiple configuration files found. Cannot continue."); + } + + if (jsonFiles.Length == 1) + { + var jsonFile = jsonFiles[0]; + var finalPath = Path.Combine(filePathPrefix, jsonFile.Name); + Debug.LogMessage(LogEventLevel.Information, + "Found configuration file: {jsonFile:l}... Moving to: {Destination}", + jsonFile.FullName, finalPath); + + if (File.Exists(finalPath)) + { + Debug.LogMessage(LogEventLevel.Information, + "Removing existing configuration file: {Destination}", finalPath); + File.Delete(finalPath); + } + + jsonFile.MoveTo(finalPath); + } + } +} diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index c91f8927..52a47273 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -271,7 +271,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig _ = new Core.DeviceFactory(); - LoadAssets(); + LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix); Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); @@ -520,201 +520,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig } - private static void LoadAssets() - { - var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); - Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.FilePathPrefix); - - var zipFiles = applicationDirectory.GetFiles("assets*.zip"); - - if (zipFiles.Length > 1) - { - throw new Exception("Multiple assets zip files found. Cannot continue."); - } - - if (zipFiles.Length == 1) - { - var zipFile = zipFiles[0]; - var assetsRoot = System.IO.Path.GetFullPath(Global.FilePathPrefix); - if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) - { - assetsRoot += Path.DirectorySeparatorChar; - } - Debug.LogMessage(LogEventLevel.Information, "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName); - using (var archive = ZipFile.OpenRead(zipFile.FullName)) - { - foreach (var entry in archive.Entries) - { - var destinationPath = Path.Combine(Global.FilePathPrefix, entry.FullName); - var fullDest = System.IO.Path.GetFullPath(destinationPath); - if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); - - if (string.IsNullOrEmpty(entry.Name)) - { - Directory.CreateDirectory(destinationPath); - continue; - } - - // If a directory exists where a file should go, delete it - if (Directory.Exists(destinationPath)) - Directory.Delete(destinationPath, true); - - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - entry.ExtractToFile(destinationPath, true); - Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); - } - } - } - - // cleaning up zip files - foreach (var file in zipFiles) - { - File.Delete(file.FullName); - } - - var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip"); - - if (htmlZipFiles.Length > 1) - { - throw new Exception("Multiple htmlassets zip files found in application directory. Please ensure only one htmlassets*.zip file is present and retry."); - } - - - if (htmlZipFiles.Length == 1) - { - var htmlZipFile = htmlZipFiles[0]; - var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); - var userOrNvramDir = programDir.Parent; - var rootDir = userOrNvramDir?.Parent; - if (rootDir == null) - { - throw new Exception($"Unable to determine root directory for html extraction. Current path: {Global.FilePathPrefix}"); - } - var htmlDir = Path.Combine(rootDir.FullName, "html"); - var htmlRoot = System.IO.Path.GetFullPath(htmlDir); - if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && - !htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) - { - htmlRoot += Path.DirectorySeparatorChar; - } - Debug.LogMessage(LogEventLevel.Information, "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName); - using (var archive = ZipFile.OpenRead(htmlZipFile.FullName)) - { - foreach (var entry in archive.Entries) - { - var destinationPath = Path.Combine(htmlDir, entry.FullName); - var fullDest = System.IO.Path.GetFullPath(destinationPath); - if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); - - if (string.IsNullOrEmpty(entry.Name)) - { - Directory.CreateDirectory(destinationPath); - continue; - } - - // Only delete the file if it exists and is a file, not a directory - if (File.Exists(destinationPath)) - File.Delete(destinationPath); - - var parentDir = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrEmpty(parentDir)) - Directory.CreateDirectory(parentDir); - - entry.ExtractToFile(destinationPath, true); - Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); - } - } - } - - // cleaning up html zip files - foreach (var file in htmlZipFiles) - { - File.Delete(file.FullName); - } - - var devToolsZipFiles = applicationDirectory.GetFiles("essentials-devtools*.zip"); - - if (devToolsZipFiles.Length > 1) - { - throw new Exception("Multiple essentials-devtools zip files found in application directory. Please ensure only one essentials-devtools*.zip file is present and retry."); - } - - if (devToolsZipFiles.Length == 1) - { - var devToolsZipFile = devToolsZipFiles[0]; - var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); - var userOrNvramDir = programDir.Parent; - var rootDir = userOrNvramDir?.Parent; - if (rootDir == null) - { - throw new Exception($"Unable to determine root directory for debug html extraction. Current path: {Global.FilePathPrefix}"); - } - var debugDir = Path.Combine(Path.Combine(rootDir.FullName, "html"), "debug"); - var debugRoot = System.IO.Path.GetFullPath(debugDir); - if (!debugRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && - !debugRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) - { - debugRoot += Path.DirectorySeparatorChar; - } - Debug.LogMessage(LogEventLevel.Information, "Found essentials-devtools zip file: {zipFile:l}... Unzipping to {Destination}...", devToolsZipFile.FullName, debugDir); - using (var archive = ZipFile.OpenRead(devToolsZipFile.FullName)) - { - foreach (var entry in archive.Entries) - { - var destinationPath = Path.Combine(debugDir, entry.FullName); - var fullDest = System.IO.Path.GetFullPath(destinationPath); - if (!fullDest.StartsWith(debugRoot, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); - - if (string.IsNullOrEmpty(entry.Name)) - { - Directory.CreateDirectory(destinationPath); - continue; - } - - if (File.Exists(destinationPath)) - File.Delete(destinationPath); - - var parentDir = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrEmpty(parentDir)) - Directory.CreateDirectory(parentDir); - - entry.ExtractToFile(destinationPath, true); - Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); - } - } - } - - // cleaning up devtools zip files - foreach (var file in devToolsZipFiles) - { - File.Delete(file.FullName); - } - - var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json"); - - if (jsonFiles.Length > 1) - { - Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", jsonFiles.Select(f => f.FullName).ToArray()); - throw new Exception("Multiple configuration files found. Cannot continue."); - } - - if (jsonFiles.Length == 1) - { - var jsonFile = jsonFiles[0]; - var finalPath = Path.Combine(Global.FilePathPrefix, jsonFile.Name); - Debug.LogMessage(LogEventLevel.Information, "Found configuration file: {jsonFile:l}... Moving to: {Destination}", jsonFile.FullName, finalPath); - - if (File.Exists(finalPath)) - { - Debug.LogMessage(LogEventLevel.Information, "Removing existing configuration file: {Destination}", finalPath); - File.Delete(finalPath); - } - - jsonFile.MoveTo(finalPath); - } - } + internal static void LoadAssets(string applicationDirectoryPath, string filePathPrefix) => + AssetLoader.Load(applicationDirectoryPath, filePathPrefix); } diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index d122c7b0..4bfd1f62 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -27,6 +27,11 @@ pdbonly bin\$(Configuration)\PepperDashEssentials.xml + + + <_Parameter1>PepperDash.Essentials.Tests + + From ec3e2bbc0b8701821bf65e3e44d2938b02b75a6f Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 9 Apr 2026 14:19:33 -0600 Subject: [PATCH 043/161] fix: Remove 'Debug 4.7.2' configuration from project files for consistency --- .../PepperDash.Essentials.Core.csproj | 2 +- .../PepperDash.Essentials.Devices.Common.csproj | 2 +- src/PepperDash.Essentials/PepperDash.Essentials.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index ee84b176..3aa65e84 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -1,6 +1,6 @@  - Debug;Release;Debug 4.7.2 + Debug;Release; net8 diff --git a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj index c5a38981..76b77f1b 100644 --- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj +++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj @@ -1,6 +1,6 @@  - Debug;Release;Debug 4.7.2 + Debug;Release; net8 diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index 4bfd1f62..29bf17c7 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -1,7 +1,7 @@  Program - Debug;Release;Debug 4.7.2 + Debug;Release; PepperDash.Essentials From 3d8ee22f45e70fd08f0719842ee70ef1bc714541 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 10 Apr 2026 09:49:16 -0600 Subject: [PATCH 044/161] feat: Refactor routing signal types and enhance web API for routing devices - Renamed `UsbOutput` to `Usb` in `eRoutingSignalType.cs`. - Removed unused `UsbInput` and `SecondaryAudio` signal types. - Added new HTTP route for retrieving routing devices and tielines in `EssentialsWebApi.cs`. - Implemented `GetRoutingDevicesAndTieLinesHandler` to handle requests for routing devices and tielines, including detailed port information. - Updated `GenericSink` to remove `SecondaryAudio` from input port signal types. - Created `ICurrentSourcesMessenger` to manage current source information and status updates. - Introduced `IDeviceInfoProviderMessenger` for device information communication with debounce functionality. - Updated `MobileControlSystemController` to use new messenger classes for current sources and device info. - Added console commands for listing tielines and visualizing routes in `ControlSystem.cs`. - Implemented methods for visualizing routes and current routes, including filtering options. --- .../Devices/DeviceManager.cs | 4 +- .../Routing/Extensions.cs | 303 +++++++- .../Routing/RouteDescriptor.cs | 77 +- .../Routing/RouteDescriptorCollection.cs | 109 +-- .../Routing/RouteSwitchDescriptor.cs | 102 +-- .../Routing/RoutingFeedbackManager.cs | 730 +++++++++++++----- .../Routing/eRoutingSignalType.cs | 11 +- .../Web/EssentialsWebApi.cs | 5 + .../GetRoutingDevicesAndTieLinesHandler.cs | 241 ++++++ .../Generic/GenericSink.cs | 2 +- ...ssenger.cs => ICurrentSourcesMessenger.cs} | 6 +- ...ger.cs => IDeviceInfoProviderMessenger.cs} | 8 +- .../MobileControlSystemController.cs | 4 +- src/PepperDash.Essentials/ControlSystem.cs | 287 +++++++ 14 files changed, 1433 insertions(+), 456 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{CurrentSourcesMessenger.cs => ICurrentSourcesMessenger.cs} (86%) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{DeviceInfoMessenger.cs => IDeviceInfoProviderMessenger.cs} (90%) diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index 7c800347..6266db5c 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -469,14 +469,14 @@ public static class DeviceManager CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine); foreach (var routingInputPort in inputPorts) { - CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine); + CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingInputPort.Key, routingInputPort.Type, CrestronEnvironment.NewLine); } } if (outputPorts == null) return; CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine); foreach (var routingOutputPort in outputPorts) { - CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine); + CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingOutputPort.Key, routingOutputPort.Type, CrestronEnvironment.NewLine); } } diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index b4335074..e2b30eee 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -17,17 +17,129 @@ namespace PepperDash.Essentials.Core; /// public static class Extensions { + /// /// Stores pending route requests, keyed by the destination device key. /// Used primarily to handle routing requests while a device is cooling down. /// private static readonly Dictionary RouteRequests = new Dictionary(); + + /// + /// A collection of RouteDescriptors for each signal type. + /// + public static readonly Dictionary RouteDescriptors = new Dictionary() + { + { eRoutingSignalType.Audio, new RouteDescriptorCollection() }, + { eRoutingSignalType.Video, new RouteDescriptorCollection() }, + { eRoutingSignalType.AudioVideo, new RouteDescriptorCollection() } + }; + /// /// A queue to process route requests and releases sequentially. /// private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue"); + /// + /// Indexed lookup of TieLines by destination device key for faster queries. + /// + private static Dictionary> _tieLinesByDestination; + + /// + /// Indexed lookup of TieLines by source device key for faster queries. + /// + private static Dictionary> _tieLinesBySource; + + /// + /// Cache of failed route attempts to avoid re-checking impossible paths. + /// Format: "sourceKey|destKey|signalType" + /// + private static readonly HashSet _impossibleRoutes = new HashSet(); + + /// + /// Indexes all TieLines by source and destination device keys for faster lookups. + /// Should be called once at system startup after all TieLines are created. + /// + public static void IndexTieLines() + { + try + { + Debug.LogMessage(LogEventLevel.Information, "Indexing TieLines for faster route discovery"); + + _tieLinesByDestination = TieLineCollection.Default + .GroupBy(t => t.DestinationPort.ParentDevice.Key) + .ToDictionary(g => g.Key, g => g.ToList()); + + _tieLinesBySource = TieLineCollection.Default + .GroupBy(t => t.SourcePort.ParentDevice.Key) + .ToDictionary(g => g.Key, g => g.ToList()); + + Debug.LogMessage(LogEventLevel.Information, "TieLine indexing complete. {0} destination keys, {1} source keys", + null, _tieLinesByDestination.Count, _tieLinesBySource.Count); + } + catch (Exception ex) + { + Debug.LogError("Exception indexing TieLines: {exception}", ex.Message); + Debug.LogDebug(ex, "Stack Trace: "); + } + } + + /// + /// Gets TieLines connected to a destination device. + /// Uses indexed lookup if available, otherwise falls back to LINQ query. + /// + /// The destination device key + /// List of TieLines connected to the destination + private static IEnumerable GetTieLinesForDestination(string destinationKey) + { + if (_tieLinesByDestination != null && _tieLinesByDestination.TryGetValue(destinationKey, out List tieLines)) + { + return tieLines; + } + + // Fallback to LINQ if index not available + return TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destinationKey); + } + + /// + /// Gets TieLines connected to a source device. + /// Uses indexed lookup if available, otherwise falls back to LINQ query. + /// + /// The source device key + /// List of TieLines connected to the source + private static IEnumerable GetTieLinesForSource(string sourceKey) + { + if (_tieLinesBySource != null && _tieLinesBySource.TryGetValue(sourceKey, out List tieLines)) + { + return tieLines; + } + + // Fallback to LINQ if index not available + return TieLineCollection.Default.Where(t => t.SourcePort.ParentDevice.Key == sourceKey); + } + + /// + /// Creates a cache key for route impossibility tracking. + /// + /// Source device key + /// Destination device key + /// Signal type + /// Cache key string + private static string GetRouteKey(string sourceKey, string destKey, eRoutingSignalType type) + { + return string.Format("{0}|{1}|{2}", sourceKey, destKey, type); + } + + /// + /// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime. + /// + public static void ClearImpossibleRoutesCache() + { + _impossibleRoutes.Clear(); + Debug.LogMessage(LogEventLevel.Information, "Impossible routes cache cleared"); + } + + /// /// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute /// and then attempts a new Route and if sucessful, stores that RouteDescriptor @@ -108,8 +220,7 @@ public static class Extensions public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) { // if it's a single signal type, find the route - if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) && - !(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio))) + if (!signalType.HasFlag(eRoutingSignalType.AudioVideo)) { var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType); Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key, signalType); @@ -129,17 +240,9 @@ public static class Extensions Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key); - RouteDescriptor audioRouteDescriptor; + var audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio); - if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio)) - { - audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio); - } else - { - audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio); - } - - var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort); + var audioSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort); if (!audioSuccess) Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key); @@ -165,10 +268,12 @@ public static class Extensions if (!audioSuccess && !videoSuccess) return (null, null); - - return (audioRouteDescriptor, videoRouteDescriptor); + // Return null for descriptors that have no routes + return (audioSuccess && audioRouteDescriptor.Routes.Count > 0 ? audioRouteDescriptor : null, + videoSuccess && videoRouteDescriptor.Routes.Count > 0 ? videoRouteDescriptor : null); } + /// /// Internal method to handle the logic for releasing an existing route and making a new one. /// Handles devices with cooling states by queueing the request. @@ -192,13 +297,13 @@ public static class Extensions Source = source, SourcePort = sourcePort, SignalType = signalType - }; + }; var coolingDevice = destination as IWarmingCooling; //We already have a route request for this device, and it's a cooling device and is cooling if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRouteRequest) && coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) - { + { coolingDevice.IsCoolingDownFeedback.OutputChange -= existingRouteRequest.HandleCooldown; coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; @@ -212,7 +317,7 @@ public static class Extensions //New Request if (coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) - { + { coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; RouteRequests.Add(destination.Key, routeRequest); @@ -232,9 +337,9 @@ public static class Extensions Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); } - routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination,destinationPort?.Key ?? string.Empty, false)); + routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false)); - routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); + routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); } /// @@ -249,7 +354,48 @@ public static class Extensions if (request.Source == null) return; - var (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort); + RouteDescriptor audioOrSingleRoute = null; + RouteDescriptor videoRoute = null; + + // Try to use pre-loaded route descriptors first + if (request.SignalType.HasFlag(eRoutingSignalType.AudioVideo)) + { + // For AudioVideo routes, check both Audio and Video collections + if (RouteDescriptors.TryGetValue(eRoutingSignalType.Audio, out RouteDescriptorCollection audioCollection)) + { + audioOrSingleRoute = audioCollection.Descriptors.FirstOrDefault(d => + d.Source.Key == request.Source.Key && + d.Destination.Key == request.Destination.Key && + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + } + + if (RouteDescriptors.TryGetValue(eRoutingSignalType.Video, out RouteDescriptorCollection videoCollection)) + { + videoRoute = videoCollection.Descriptors.FirstOrDefault(d => + d.Source.Key == request.Source.Key && + d.Destination.Key == request.Destination.Key && + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + } + } + else + { + // For single signal type routes + if (RouteDescriptors.TryGetValue(request.SignalType, out RouteDescriptorCollection collection)) + { + audioOrSingleRoute = collection.Descriptors.FirstOrDefault(d => + d.Source.Key == request.Source.Key && + d.Destination.Key == request.Destination.Key && + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + } + } + + // If no pre-loaded route found, build it dynamically + if (audioOrSingleRoute == null && videoRoute == null) + { + Debug.LogMessage(LogEventLevel.Debug, "No pre-loaded route found, building dynamically", request.Destination); + (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort); + } + if (audioOrSingleRoute == null && videoRoute == null) return; @@ -265,7 +411,8 @@ public static class Extensions audioOrSingleRoute.ExecuteRoutes(); videoRoute?.ExecuteRoutes(); - } catch(Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request); } @@ -298,9 +445,86 @@ public static class Extensions Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key); current.ReleaseRoutes(clearRoute); } - } catch (Exception ex) + } + catch (Exception ex) { - Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'",null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'", null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + } + } + + /// + /// Maps destination input ports to source output ports for all routing devices. + /// + public static void MapDestinationsToSources() + { + try + { + // Index TieLines before mapping if not already done + if (_tieLinesByDestination == null || _tieLinesBySource == null) + { + IndexTieLines(); + } + + var sinks = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); + var sources = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); + + foreach (var sink in sinks) + { + foreach (var source in sources) + { + foreach (var inputPort in sink.InputPorts) + { + foreach (var outputPort in source.OutputPorts) + { + var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, inputPort.Type, inputPort, outputPort); + + if (audioOrSingleRoute == null && videoRoute == null) + { + continue; + } + + if (audioOrSingleRoute != null) + { + // Only add routes that have actual switching steps + if (audioOrSingleRoute.Routes == null || audioOrSingleRoute.Routes.Count == 0) + { + continue; + } + + // Add to the appropriate collection(s) based on signal type + // Note: A single route descriptor with combined flags (e.g., AudioVideo) will be added once per matching signal type + if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Audio)) + { + RouteDescriptors[eRoutingSignalType.Audio].AddRouteDescriptor(audioOrSingleRoute); + } + if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Video)) + { + RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(audioOrSingleRoute); + } + if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Usb)) + { + RouteDescriptors[eRoutingSignalType.Usb].AddRouteDescriptor(audioOrSingleRoute); + } + if (videoRoute != null) + { + // Only add routes that have actual switching steps + if (videoRoute.Routes == null || videoRoute.Routes.Count == 0) + { + continue; + } + + RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute); + } + } + } + } + } + } + } + catch (Exception ex) + { + Debug.LogError("Exception mapping routes: {exception}", ex.Message); + Debug.LogDebug(ex, "Stack Trace: "); } } @@ -311,9 +535,9 @@ public static class Extensions /// /// /// - /// The RoutingOutputPort whose link is being checked for a route + /// The RoutingInputPort whose link is being checked for a route /// The RoutingOutputPort whose link is being checked for a route - /// The RoutingOutputPort to use for the route + /// The RoutingOutputPort whose link is being checked for a route /// Prevents Devices from being twice-checked /// This recursive function should not be called with AudioVideo /// Just an informational counter @@ -325,42 +549,53 @@ public static class Extensions { cycle++; + var routeKey = GetRouteKey(source.Key, destination.Key, signalType); + if (_impossibleRoutes.Contains(routeKey)) + { + Debug.LogMessage(LogEventLevel.Verbose, "Route {0} is cached as impossible, skipping", null, routeKey); + return false; + } + Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString()); RoutingInputPort goodInputPort = null; + // Use indexed lookup instead of LINQ query + var allDestinationTieLines = GetTieLinesForDestination(destination.Key); + IEnumerable destinationTieLines; TieLine directTie = null; if (destinationPort == null) { - destinationTieLines = TieLineCollection.Default.Where(t => - t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo)); + destinationTieLines = allDestinationTieLines.Where(t => + t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo); } else { - destinationTieLines = TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && (t.Type.HasFlag(signalType))); + destinationTieLines = allDestinationTieLines.Where(t => + t.DestinationPort.Key == destinationPort.Key && t.Type.HasFlag(signalType)); } // find the TieLine without a port if (destinationPort == null && sourcePort == null) { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key); + directTie = destinationTieLines.FirstOrDefault(t => t.SourcePort.ParentDevice.Key == source.Key); } // find a tieLine to a specific destination port without a specific source port else if (destinationPort != null && sourcePort == null) { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key); + directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key); } // find a tieline to a specific source port without a specific destination port else if (destinationPort == null & sourcePort != null) { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); + directTie = destinationTieLines.FirstOrDefault(t => t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); } // find a tieline to a specific source port and destination port else if (destinationPort != null && sourcePort != null) { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); + directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); } if (directTie != null) // Found a tie directly to the source @@ -415,6 +650,10 @@ public static class Extensions if (goodInputPort == null) { Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key); + + // Cache this as an impossible route + _impossibleRoutes.Add(routeKey); + return false; } diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs index c69285dd..fa68a42d 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs @@ -101,7 +101,7 @@ public class RouteDescriptor { if (route.SwitchingDevice is IRouting switchingDevice) { - if(clearRoute) + if (clearRoute) { try { @@ -138,77 +138,6 @@ public class RouteDescriptor public override string ToString() { var routesText = Routes.Select(r => r.ToString()).ToArray(); - return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); + return $"Route table from {Source.Key} to {Destination.Key} for {SignalType}:\r\n {string.Join("\r\n ", routesText)}"; } -} - -/*/// -/// Represents an collection of individual route steps between Source and Destination -/// -public class RouteDescriptor -{ - public IRoutingInputs Destination { get; private set; } - public IRoutingOutputs Source { get; private set; } - public eRoutingSignalType SignalType { get; private set; } - public List> Routes { get; private set; } - - - public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) - { - Destination = destination; - Source = source; - SignalType = signalType; - Routes = new List>(); - } - - /// - /// Executes all routes described in this collection. Typically called via - /// extension method IRoutingInputs.ReleaseAndMakeRoute() - /// - public void ExecuteRoutes() - { - foreach (var route in Routes) - { - Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString()); - - if (route.SwitchingDevice is IRoutingSinkWithSwitching sink) - { - sink.ExecuteSwitch(route.InputPort.Selector); - continue; - } - - if (route.SwitchingDevice is IRouting switchingDevice) - { - switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); - - route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); - - Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } - } - } - - /// - /// Releases all routes in this collection. Typically called via - /// extension method IRoutingInputs.ReleaseAndMakeRoute() - /// - public void ReleaseRoutes() - { - foreach (var route in Routes) - { - if (route.SwitchingDevice is IRouting) - { - // Pull the route from the port. Whatever is watching the output's in use tracker is - // responsible for responding appropriately. - route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); - Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } - } - } - - public override string ToString() - { - var routesText = Routes.Select(r => r.ToString()).ToArray(); - return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); - } -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs index 81f3aa6a..9cf61f77 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs @@ -11,6 +11,9 @@ namespace PepperDash.Essentials.Core; /// public class RouteDescriptorCollection { + /// + /// The static default collection of RouteDescriptors. This is typically used for global routing management across the system, but additional collections could be used for specific purposes if desired. + /// public static RouteDescriptorCollection DefaultCollection { get @@ -24,6 +27,12 @@ public class RouteDescriptorCollection private readonly List RouteDescriptors = new List(); + /// + /// Gets an enumerable collection of all RouteDescriptors in this collection. + /// + public IEnumerable Descriptors => RouteDescriptors.AsReadOnly(); + + /// /// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the /// destination exists already, it will not be added - in order to preserve @@ -37,13 +46,29 @@ public class RouteDescriptorCollection return; } - if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination) - && RouteDescriptors.Any(t => t.Destination == descriptor.Destination && t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key)) + // Check if a route already exists with the same source, destination, input port, AND signal type + var existingRoute = RouteDescriptors.FirstOrDefault(t => + t.Source == descriptor.Source && + t.Destination == descriptor.Destination && + t.SignalType == descriptor.SignalType && + ((t.InputPort == null && descriptor.InputPort == null) || + (t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key))); + + if (existingRoute != null) { - Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination, - "Route to [{0}] already exists in global routes table", descriptor?.Source?.Key); + Debug.LogMessage(LogEventLevel.Information, descriptor.Destination, + "Route from {0} to {1}:{2} ({3}) already exists in this collection", + descriptor?.Source?.Key, + descriptor?.Destination?.Key, + descriptor?.InputPort?.Key ?? "auto", + descriptor?.SignalType); return; } + Debug.LogMessage(LogEventLevel.Verbose, "Adding route descriptor: {0} -> {1}:{2} ({3})", + descriptor?.Source?.Key, + descriptor?.Destination?.Key, + descriptor?.InputPort?.Key ?? "auto", + descriptor?.SignalType); RouteDescriptors.Add(descriptor); } @@ -58,6 +83,12 @@ public class RouteDescriptorCollection return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination); } + /// + /// Gets the route descriptor for a specific destination and input port + /// + /// The destination device + /// The input port key + /// The matching RouteDescriptor or null if not found public RouteDescriptor GetRouteDescriptorForDestinationAndInputPort(IRoutingInputs destination, string inputPortKey) { Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); @@ -68,11 +99,14 @@ public class RouteDescriptorCollection /// Returns the RouteDescriptor for a given destination AND removes it from collection. /// Returns null if no route with the provided destination exists. /// + /// The destination device + /// The input port key (optional) + /// The matching RouteDescriptor or null if not found public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination, string inputPortKey = "") { Debug.LogMessage(LogEventLevel.Information, "Removing route descriptor for '{destination}':'{inputPortKey}'", destination.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); - var descr = string.IsNullOrEmpty(inputPortKey) + var descr = string.IsNullOrEmpty(inputPortKey) ? GetRouteDescriptorForDestination(destination) : GetRouteDescriptorForDestinationAndInputPort(destination, inputPortKey); if (descr != null) @@ -82,67 +116,4 @@ public class RouteDescriptorCollection return descr; } -} - -/*/// -/// A collection of RouteDescriptors - typically the static DefaultCollection is used -/// -public class RouteDescriptorCollection - { - public static RouteDescriptorCollection DefaultCollection - { - get - { - if (_DefaultCollection == null) - _DefaultCollection = new RouteDescriptorCollection(); - return _DefaultCollection; - } - } - private static RouteDescriptorCollection _DefaultCollection; - - private readonly List RouteDescriptors = new List(); - - /// - /// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the - /// destination exists already, it will not be added - in order to preserve - /// proper route releasing. - /// - /// - /// - /// AddRouteDescriptor method - /// - public void AddRouteDescriptor(RouteDescriptor descriptor) - { - if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination)) - { - Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination, - "Route to [{0}] already exists in global routes table", descriptor.Source.Key); - return; - } - RouteDescriptors.Add(descriptor); - } - - /// - /// Gets the RouteDescriptor for a destination - /// - /// null if no RouteDescriptor for a destination exists - /// - /// GetRouteDescriptorForDestination method - /// - public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs destination) - { - return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination); - } - - /// - /// Returns the RouteDescriptor for a given destination AND removes it from collection. - /// Returns null if no route with the provided destination exists. - /// - public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination) - { - var descr = GetRouteDescriptorForDestination(destination); - if (descr != null) - RouteDescriptors.Remove(descr); - return descr; - } - }*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs index 03759018..227e6d5b 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs @@ -1,54 +1,54 @@ -namespace PepperDash.Essentials.Core; +namespace PepperDash.Essentials.Core +{ + /// + /// Represents a RouteSwitchDescriptor + /// + public class RouteSwitchDescriptor + { + /// + /// Gets or sets the SwitchingDevice + /// + public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } } + /// + /// The output port being switched from (relevant for matrix switchers). Null for sink devices. + /// + public RoutingOutputPort OutputPort { get; set; } + /// + /// The input port being switched to. + /// + public RoutingInputPort InputPort { get; set; } -/// -/// Represents a single switching step within a larger route, detailing the switching device, input port, and optionally the output port. -/// -public class RouteSwitchDescriptor - { - /// - /// Gets or sets the SwitchingDevice - /// - public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } } - /// - /// The output port being switched from (relevant for matrix switchers). Null for sink devices. - /// - public RoutingOutputPort OutputPort { get; set; } - /// - /// The input port being switched to. - /// - public RoutingInputPort InputPort { get; set; } + /// + /// Initializes a new instance of the class for sink devices (no output port). + /// + /// The input port being switched to. + public RouteSwitchDescriptor(RoutingInputPort inputPort) + { + InputPort = inputPort; + } - /// - /// Initializes a new instance of the class for sink devices (no output port). - /// - /// The input port being switched to. - public RouteSwitchDescriptor(RoutingInputPort inputPort) - { - InputPort = inputPort; - } - - /// - /// Initializes a new instance of the class for matrix switchers. - /// - /// The output port being switched from. - /// The input port being switched to. - public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort) - { - InputPort = inputPort; - OutputPort = outputPort; - } - - /// - /// Returns a string representation of the route switch descriptor. - /// - /// A string describing the switch operation. - /// - public override string ToString() - { - if (SwitchingDevice is IRouting) - return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}"; - else - return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}"; - } - } + /// + /// Initializes a new instance of the class for matrix switchers. + /// + /// The output port being switched from. + /// The input port being switched to. + public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort) + { + InputPort = inputPort; + OutputPort = outputPort; + } + /// + /// Returns a string representation of the route switch descriptor. + /// + /// A string describing the switch operation. + /// + public override string ToString() + { + if (SwitchingDevice is IRouting) + return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}"; + else + return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}"; + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 69f41d64..73401600 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -1,248 +1,562 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Timers; using PepperDash.Core; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials.Core.Routing; - -/// -/// Manages routing feedback by subscribing to route changes on midpoint and sink devices, -/// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. -/// -public class RoutingFeedbackManager : EssentialsDevice +namespace PepperDash.Essentials.Core.Routing { /// - /// Initializes a new instance of the class. + /// Manages routing feedback by subscribing to route changes on midpoint and sink devices, + /// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. /// - /// The unique key for this manager device. - /// The name of this manager device. - public RoutingFeedbackManager(string key, string name) : base(key, name) + public class RoutingFeedbackManager : EssentialsDevice { - AddPreActivationAction(SubscribeForMidpointFeedback); - AddPreActivationAction(SubscribeForSinkFeedback); - } + /// + /// Maps midpoint device keys to the set of sink device keys that are downstream + /// + private Dictionary> midpointToSinksMap; + /// + /// Debounce timers for each sink device to prevent rapid successive updates + /// + private readonly Dictionary updateTimers = new Dictionary(); - /// - /// Subscribes to the RouteChanged event on all devices implementing . - /// - private void SubscribeForMidpointFeedback() - { - var midpointDevices = DeviceManager.AllDevices.OfType(); + /// + /// Debounce delay in milliseconds + /// + private const long DEBOUNCE_MS = 500; - foreach (var device in midpointDevices) + /// + /// Initializes a new instance of the class. + /// + /// The unique key for this manager device. + /// The name of this manager device. + public RoutingFeedbackManager(string key, string name) + : base(key, name) { - device.RouteChanged += HandleMidpointUpdate; + AddPreActivationAction(BuildMidpointSinkMap); + AddPreActivationAction(SubscribeForMidpointFeedback); + AddPreActivationAction(SubscribeForSinkFeedback); } - } - /// - /// Subscribes to the InputChanged event on all devices implementing . - /// - private void SubscribeForSinkFeedback() - { - var sinkDevices = DeviceManager.AllDevices.OfType(); - - foreach (var device in sinkDevices) + /// + /// Builds a map of which sink devices are downstream of each midpoint device + /// for performance optimization in HandleMidpointUpdate + /// + private void BuildMidpointSinkMap() { - device.InputChanged += HandleSinkUpdate; - } - } + midpointToSinksMap = new Dictionary>(); - /// - /// Handles the RouteChanged event from a midpoint device. - /// Triggers an update for all sink devices. - /// - /// The midpoint device that reported a route change. - /// The descriptor of the new route. - private void HandleMidpointUpdate(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute) - { - try - { - var devices = DeviceManager.AllDevices.OfType(); + var sinks = DeviceManager.AllDevices.OfType(); + var midpoints = DeviceManager.AllDevices.OfType(); - foreach (var device in devices) + foreach (var sink in sinks) { - UpdateDestination(device, device.CurrentInputPort); + if (sink.CurrentInputPort == null) + continue; + + // Find all upstream midpoints for this sink + var upstreamMidpoints = GetUpstreamMidpoints(sink); + + foreach (var midpointKey in upstreamMidpoints) + { + if (!midpointToSinksMap.ContainsKey(midpointKey)) + midpointToSinksMap[midpointKey] = new HashSet(); + + midpointToSinksMap[midpointKey].Add(sink.Key); + } } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error handling midpoint update from {midpointKey}:{Exception}", this, midpoint.Key, ex); - } - } - /// - /// Handles the InputChanged event from a sink device. - /// Triggers an update for the specific sink device. - /// - /// The sink device that reported an input change. - /// The new input port selected on the sink device. - private void HandleSinkUpdate(IRoutingSinkWithSwitching sender, RoutingInputPort currentInputPort) - { - try - { - UpdateDestination(sender, currentInputPort); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error handling Sink update from {senderKey}:{Exception}", this, sender.Key, ex); - } - } - - /// - /// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device - /// based on its currently selected input port by tracing the route back through tie lines. - /// - /// The destination sink device to update. - /// The currently selected input port on the destination device. - private void UpdateDestination(IRoutingSink destination, RoutingInputPort inputPort) - { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key); - - if (inputPort == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Destination {destination} has not reported an input port yet", this, destination.Key); - return; + Debug.LogMessage( + Serilog.Events.LogEventLevel.Information, + "Built midpoint-to-sink map with {count} midpoints", + this, + midpointToSinksMap.Count + ); } - TieLine firstTieLine; - try + /// + /// Gets all upstream midpoint device keys for a given sink + /// + private HashSet GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink) { - var tieLines = TieLineCollection.Default; + var result = new HashSet(); + var visited = new HashSet(); - firstTieLine = tieLines.FirstOrDefault(tl => tl.DestinationPort.Key == inputPort.Key && tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key); + if (sink.CurrentInputPort == null) + return result; - if (firstTieLine == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No tieline found for inputPort {inputPort}. Clearing current source", this, inputPort); + var tieLine = TieLineCollection.Default.FirstOrDefault(tl => + tl.DestinationPort.Key == sink.CurrentInputPort.Key && + tl.DestinationPort.ParentDevice.Key == sink.CurrentInputPort.ParentDevice.Key); + + if (tieLine == null) + return result; + + TraceUpstreamMidpoints(tieLine, result, visited); + return result; + } + + /// + /// Recursively traces upstream to find all midpoint devices + /// + private void TraceUpstreamMidpoints(TieLine tieLine, HashSet midpoints, HashSet visited) + { + if (tieLine == null || visited.Contains(tieLine.SourcePort.ParentDevice.Key)) return; - } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); - return; - } - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Getting source for first TieLine {tieLine}", this, firstTieLine); - - - TieLine sourceTieLine; - try - { - sourceTieLine = GetRootTieLine(firstTieLine); - - if (sourceTieLine == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route found to source for inputPort {inputPort}. Clearing current source", this, inputPort); - - - destination.SetCurrentSource(sourceTieLine.Type, null); - - - return; - } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); - return; - } - - if (sourceTieLine.SourcePort.ParentDevice == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No source found for device {key}. Creating transient source for {destination}", this, sourceTieLine.SourcePort.ParentDevice.Key, destination); - - destination.SetCurrentSource(sourceTieLine.Type, null); - - return; - } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Got Source {@source} with key {sourceKey}", this, source, sourceKey); - - if (sourceTieLine.SourcePort.ParentDevice is IRoutingSource sourceDevice) - { - destination.SetCurrentSource(sourceTieLine.Type, sourceDevice); - } - } - - /// - /// Recursively traces a route back from a given tie line to find the root source tie line. - /// It navigates through midpoint devices () by checking their current routes. - /// - /// The starting tie line (typically connected to a sink or midpoint). - /// The connected to the original source device, or null if the source cannot be determined. - private TieLine GetRootTieLine(TieLine tieLine) - { - TieLine nextTieLine = null; - try - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine); + visited.Add(tieLine.SourcePort.ParentDevice.Key); if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint) { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint); + midpoints.Add(midpoint.Key); - if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0) + // Find upstream TieLines connected to this midpoint's inputs + var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts; + if (midpointInputs != null) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Midpoint {midpointKey} has no routes", this, midpoint.Key); - return null; + foreach (var inputPort in midpointInputs) + { + var upstreamTieLine = TieLineCollection.Default.FirstOrDefault(tl => + tl.DestinationPort.Key == inputPort.Key && + tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key); + + if (upstreamTieLine != null) + TraceUpstreamMidpoints(upstreamTieLine, midpoints, visited); + } } - - var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route => - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine); - - return route.OutputPort != null && route.InputPort != null && route.OutputPort?.Key == tieLine.SourcePort.Key && route.OutputPort?.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key; - }); - - if (currentRoute == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route through midpoint {midpoint} for outputPort {outputPort}", this, midpoint.Key, tieLine.SourcePort); - return null; - } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint); - - nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key); - return tl.DestinationPort.Key == currentRoute.InputPort.Key && tl.DestinationPort.ParentDevice.Key == currentRoute.InputPort.ParentDevice.Key; - }); - - if (nextTieLine != null) - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found next tieLine {tieLine}. Walking the chain", this, nextTieLine); - return GetRootTieLine(nextTieLine); - } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root tieLine {tieLine}", this,nextTieLine); - return nextTieLine; - } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLIne Source Device {sourceDeviceKey} is IRoutingSource: {isIRoutingSource}", this, tieLine.SourcePort.ParentDevice.Key, tieLine.SourcePort.ParentDevice is IRoutingSource); - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source Device interfaces: {typeFullName}:{interfaces}", this, tieLine.SourcePort.ParentDevice.GetType().FullName, tieLine.SourcePort.ParentDevice.GetType().GetInterfaces().Select(i => i.Name)); - - if (tieLine.SourcePort.ParentDevice is IRoutingSource || tieLine.SourcePort.ParentDevice is IRoutingOutputs) //end of the chain - { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root: {tieLine}", this, tieLine); - return tieLine; - } - - nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => tl.DestinationPort.Key == tieLine.SourcePort.Key && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key); - - if (nextTieLine != null) - { - return GetRootTieLine(nextTieLine); } } - catch (Exception ex) + + /// + /// Subscribes to the RouteChanged event on all devices implementing . + /// + private void SubscribeForMidpointFeedback() { - Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex); - return null; + var midpointDevices = DeviceManager.AllDevices.OfType(); + + foreach (var device in midpointDevices) + { + device.RouteChanged += HandleMidpointUpdate; + } } - return null; + /// + /// Subscribes to the InputChanged event on all devices implementing . + /// + private void SubscribeForSinkFeedback() + { + var sinkDevices = + DeviceManager.AllDevices.OfType(); + + foreach (var device in sinkDevices) + { + device.InputChanged += HandleSinkUpdate; + } + } + + /// + /// Handles the RouteChanged event from a midpoint device. + /// Only triggers updates for sink devices that are downstream of this midpoint. + /// + /// The midpoint device that reported a route change. + /// The descriptor of the new route. + private void HandleMidpointUpdate( + IRoutingWithFeedback midpoint, + RouteSwitchDescriptor newRoute + ) + { + try + { + // Only update affected sinks (performance optimization) + if (midpointToSinksMap != null && midpointToSinksMap.TryGetValue(midpoint.Key, out var affectedSinkKeys)) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Midpoint {midpoint} changed, updating {count} downstream sinks", + this, + midpoint.Key, + affectedSinkKeys.Count + ); + + foreach (var sinkKey in affectedSinkKeys) + { + if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithSwitchingWithInputPort sink) + { + UpdateDestination(sink, sink.CurrentInputPort); + } + } + } + else + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Midpoint {midpoint} changed but has no downstream sinks in map", + this, + midpoint.Key + ); + } + } + catch (Exception ex) + { + Debug.LogMessage( + ex, + "Error handling midpoint update from {midpointKey}:{Exception}", + this, + midpoint.Key, + ex + ); + } + } + + /// + /// Handles the InputChanged event from a sink device. + /// Triggers an update for the specific sink device. + /// + /// The sink device that reported an input change. + /// The new input port selected on the sink device. + private void HandleSinkUpdate( + IRoutingSinkWithSwitching sender, + RoutingInputPort currentInputPort + ) + { + try + { + UpdateDestination(sender, currentInputPort); + } + catch (Exception ex) + { + Debug.LogMessage( + ex, + "Error handling Sink update from {senderKey}:{Exception}", + this, + sender.Key, + ex + ); + } + } + + /// + /// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device + /// based on its currently selected input port by tracing the route back through tie lines. + /// Uses debouncing to prevent rapid successive updates. + /// + /// The destination sink device to update. + /// The currently selected input port on the destination device. + private void UpdateDestination( + IRoutingSinkWithSwitching destination, + RoutingInputPort inputPort + ) + { + if (destination == null) + return; + + var key = destination.Key; + + // Cancel existing timer for this sink + if (updateTimers.TryGetValue(key, out var existingTimer)) + { + existingTimer.Stop(); + existingTimer.Dispose(); + } + + // Start new debounced timer + var timer = new Timer(DEBOUNCE_MS) { AutoReset = false }; + timer.Elapsed += (sender, e) => + { + try + { + UpdateDestinationImmediate(destination, inputPort); + } + catch (Exception ex) + { + Debug.LogMessage( + ex, + "Error in debounced update for destination {destinationKey}: {message}", + this, + destination.Key, + ex.Message + ); + } + finally + { + if (updateTimers.ContainsKey(key)) + { + updateTimers[key]?.Dispose(); + updateTimers.Remove(key); + } + } + }; + timer.Start(); + updateTimers[key] = timer; + } + + /// + /// Immediately updates the CurrentSourceInfo for a destination device. + /// Called after debounce delay. + /// + private void UpdateDestinationImmediate( + IRoutingSinkWithSwitching destination, + RoutingInputPort inputPort + ) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Updating destination {destination} with inputPort {inputPort}", + this, + destination?.Key, + inputPort?.Key + ); + + if (inputPort == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Destination {destination} has not reported an input port yet", + this, + destination.Key + ); + return; + } + + TieLine firstTieLine; + try + { + var tieLines = TieLineCollection.Default; + + firstTieLine = tieLines.FirstOrDefault(tl => + tl.DestinationPort.Key == inputPort.Key + && tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key + ); + + if (firstTieLine == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No tieline found for inputPort {inputPort}. Clearing current source", + this, + inputPort + ); + + var tempSourceListItem = new SourceListItem + { + SourceKey = "$transient", + Name = inputPort.Key, + }; + + return; + } + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Getting source for first TieLine {tieLine}", this, firstTieLine); + + TieLine sourceTieLine; + try + { + sourceTieLine = GetRootTieLine(firstTieLine); + + if (sourceTieLine == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No route found to source for inputPort {inputPort}. Clearing current source", + this, + inputPort + ); + + var tempSourceListItem = new SourceListItem + { + SourceKey = "$transient", + Name = "None", + }; + + return; + } + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine); + + // Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet. + var room = DeviceManager + .AllDevices.OfType() + .FirstOrDefault( + (r) => + { + + if (r is IHasDefaultDisplay roomDefaultDisplay) + { + return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; + } + + return false; + } + ); + + if (room == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No room found for display {destination}", + this, + destination.Key + ); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found room {room} for destination {destination}", this, room.Key, destination.Key); + + var sourceList = ConfigReader.ConfigObject.GetSourceListForKey(room.SourceListKey); + + if (sourceList == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", + this, + room.SourceListKey, + sourceTieLine + ); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key); + + var sourceListItem = sourceList.FirstOrDefault(sli => + { + //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, + // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", + // this, + // sli.Key, + // sli.Value.SourceKey, + // sourceTieLine.SourcePort.ParentDevice.Key); + + return sli.Value.SourceKey.Equals( + sourceTieLine.SourcePort.ParentDevice.Key, + StringComparison.InvariantCultureIgnoreCase + ); + }); + + var source = sourceListItem.Value; + var sourceKey = sourceListItem.Key; + + if (source == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No source found for device {key}. Creating transient source for {destination}", + this, + sourceTieLine.SourcePort.ParentDevice.Key, + destination + ); + + + return; + } + + } + + /// + /// Traces a route back from a given tie line to find the root source tie line. + /// Leverages the existing Extensions.GetRouteToSource method with loop protection. + /// + /// The starting tie line (typically connected to a sink or midpoint). + /// The connected to the original source device, or null if the source cannot be determined. + private TieLine GetRootTieLine(TieLine tieLine) + { + try + { + if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink)) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "TieLine destination {device} is not IRoutingInputs", + this, + tieLine.DestinationPort.ParentDevice.Key + ); + return null; + } + + // Get all potential sources (devices that only have outputs, not inputs+outputs) + var sources = DeviceManager.AllDevices + .OfType() + .Where(s => !(s is IRoutingInputsOutputs)); + + // Try each signal type that this TieLine supports + var signalTypes = new[] + { + eRoutingSignalType.Audio, + eRoutingSignalType.Video, + eRoutingSignalType.AudioVideo, + }; + + foreach (var signalType in signalTypes) + { + if (!tieLine.Type.HasFlag(signalType)) + continue; + + foreach (var source in sources) + { + // Use the optimized route discovery with loop protection + var (route, _) = sink.GetRouteToSource( + source, + signalType, + tieLine.DestinationPort, + null + ); + + if (route != null && route.Routes != null && route.Routes.Count > 0) + { + // Found a valid route - return the source TieLine + var sourceTieLine = TieLineCollection.Default.FirstOrDefault(tl => + tl.SourcePort.ParentDevice.Key == source.Key && + tl.Type.HasFlag(signalType)); + + if (sourceTieLine != null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Found route from {source} to {sink} with {count} hops", + this, + source.Key, + sink.Key, + route.Routes.Count + ); + return sourceTieLine; + } + } + } + } + + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No route found to any source from {sink}", + this, + sink.Key + ); + return null; + } + catch (Exception ex) + { + Debug.LogMessage( + ex, + "Error getting root tieLine: {Exception}", + this, + ex + ); + return null; + } + } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs b/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs index e3d679da..f3c346dc 100644 --- a/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs +++ b/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs @@ -27,16 +27,7 @@ namespace PepperDash.Essentials.Core /// /// Control signal type /// - UsbOutput = 8, + Usb = 8, - /// - /// Control signal type - /// - UsbInput = 16, - - /// - /// Secondary audio signal type - /// - SecondaryAudio = 32 } } diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index 8a66431b..f661a7ff 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -179,6 +179,11 @@ public class EssentialsWebApi : EssentialsDevice Name = "Get Routing Ports for a device", RouteHandler = new GetRoutingPortsHandler() }, + new HttpCwsRoute("routingDevicesAndTieLines") + { + Name = "Get Routing Devices and TieLines", + RouteHandler = new GetRoutingDevicesAndTieLinesHandler() + }, }; AddRoute(routes); diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs new file mode 100644 index 00000000..72a6757f --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs @@ -0,0 +1,241 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Web.RequestHandlers; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers +{ + /// + /// Handles HTTP requests to retrieve routing devices and tielines information + /// + public class GetRoutingDevicesAndTieLinesHandler : WebApiBaseRequestHandler + { + + /// + /// Initializes a new instance of the class. + /// + public GetRoutingDevicesAndTieLinesHandler() : base(true) { } + + /// + /// Handles the GET request to retrieve routing devices and tielines information + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var devices = new List(); + + // Get all devices from DeviceManager + foreach (var device in DeviceManager.AllDevices) + { + var deviceInfo = new RoutingDeviceInfo + { + Key = device.Key, + Name = (device as IKeyName)?.Name ?? device.Key + }; + + // Check if device implements IRoutingInputs + if (device is IRoutingInputs inputDevice) + { + deviceInfo.HasInputs = true; + deviceInfo.InputPorts = inputDevice.InputPorts.Select(p => new PortInfo + { + Key = p.Key, + SignalType = p.Type.ToString(), + ConnectionType = p.ConnectionType.ToString(), + IsInternal = p.IsInternal + }).ToList(); + } + + // Check if device implements IRoutingOutputs + if (device is IRoutingOutputs outputDevice) + { + deviceInfo.HasOutputs = true; + deviceInfo.OutputPorts = outputDevice.OutputPorts.Select(p => new PortInfo + { + Key = p.Key, + SignalType = p.Type.ToString(), + ConnectionType = p.ConnectionType.ToString(), + IsInternal = p.IsInternal + }).ToList(); + } + + // Check if device implements IRoutingInputsOutputs + if (device is IRoutingInputsOutputs) + { + deviceInfo.HasInputsAndOutputs = true; + } + + // Only include devices that have routing capabilities + if (deviceInfo.HasInputs || deviceInfo.HasOutputs) + { + devices.Add(deviceInfo); + } + } + + // Get all tielines + var tielines = TieLineCollection.Default.Select(tl => new TieLineInfo + { + SourceDeviceKey = tl.SourcePort.ParentDevice.Key, + SourcePortKey = tl.SourcePort.Key, + DestinationDeviceKey = tl.DestinationPort.ParentDevice.Key, + DestinationPortKey = tl.DestinationPort.Key, + SignalType = tl.Type.ToString(), + IsInternal = tl.IsInternal + }).ToList(); + + var response = new RoutingSystemInfo + { + Devices = devices, + TieLines = tielines + }; + + var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(jsonResponse, false); + context.Response.End(); + } + } + + /// + /// Represents the complete routing system information including devices and tielines + /// + public class RoutingSystemInfo + { + + /// + /// Gets or sets the list of routing devices in the system, including their ports information + /// + [JsonProperty("devices")] + public List Devices { get; set; } + + + /// + /// Gets or sets the list of tielines in the system, including source/destination device and port information + /// + [JsonProperty("tieLines")] + public List TieLines { get; set; } + } + + /// + /// Represents a routing device with its ports information + /// + public class RoutingDeviceInfo : IKeyName + { + + /// + [JsonProperty("key")] + public string Key { get; set; } + + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether the device has routing input ports + /// + [JsonProperty("hasInputs")] + public bool HasInputs { get; set; } + + /// + /// Gets or sets a value indicating whether the device has routing output ports + /// + [JsonProperty("hasOutputs")] + public bool HasOutputs { get; set; } + + /// + /// Gets or sets a value indicating whether the device has both routing inputs and outputs (e.g., matrix switcher) + /// + [JsonProperty("hasInputsAndOutputs")] + public bool HasInputsAndOutputs { get; set; } + + /// + /// Gets or sets the list of input ports for the device, if applicable. Null if the device does not have routing inputs. + /// + [JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)] + public List InputPorts { get; set; } + + /// + /// Gets or sets the list of output ports for the device, if applicable. Null if the device does not have routing outputs. + /// + [JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)] + public List OutputPorts { get; set; } + } + + /// + /// Represents a routing port with its properties + /// + public class PortInfo : IKeyed + { + /// + [JsonProperty("key")] + public string Key { get; set; } + + /// + /// Gets or sets the signal type of the port (e.g., AudioVideo, Audio, Video, etc.) + /// + [JsonProperty("signalType")] + public string SignalType { get; set; } + + /// + /// Gets or sets the connection type of the port (e.g., Hdmi, Dvi, Vga, etc.) + /// + [JsonProperty("connectionType")] + public string ConnectionType { get; set; } + + /// + /// Gets or sets a value indicating whether the port is internal + /// + [JsonProperty("isInternal")] + public bool IsInternal { get; set; } + } + + /// + /// Represents a tieline connection between two ports + /// + public class TieLineInfo + { + /// + /// Gets or sets the key of the source device for the tieline connection + /// + [JsonProperty("sourceDeviceKey")] + public string SourceDeviceKey { get; set; } + + + /// + /// Gets or sets the key of the source port for the tieline connection + /// + [JsonProperty("sourcePortKey")] + public string SourcePortKey { get; set; } + + /// + /// Gets or sets the key of the destination device for the tieline connection + /// + [JsonProperty("destinationDeviceKey")] + public string DestinationDeviceKey { get; set; } + + /// + /// Gets or sets the key of the destination port for the tieline connection + /// + [JsonProperty("destinationPortKey")] + public string DestinationPortKey { get; set; } + + /// + /// Gets or sets the signal type of the tieline connection (e.g., AudioVideo, Audio, Video, etc.) + /// + [JsonProperty("signalType")] + public string SignalType { get; set; } + + /// + /// Gets or sets a value indicating whether the tieline connection is internal + /// + [JsonProperty("isInternal")] + public bool IsInternal { get; set; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index 46507a98..7e449564 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -32,7 +32,7 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputP { InputPorts = new RoutingPortCollection(); - var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this); + var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); InputPorts.Add(inputPort); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs similarity index 86% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs index e2c1473d..ae00d78a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs @@ -12,17 +12,17 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Represents a IHasCurrentSourceInfoMessenger /// - public class CurrentSourcesMessenger : MessengerBase + public class ICurrentSourcesMessenger : MessengerBase { private readonly ICurrentSources sourceDevice; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The key. /// The message path. /// The device. - public CurrentSourcesMessenger(string key, string messagePath, ICurrentSources device) : base(key, messagePath, device as IKeyName) + public ICurrentSourcesMessenger(string key, string messagePath, ICurrentSources device) : base(key, messagePath, device as IKeyName) { sourceDevice = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs similarity index 90% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs index 15e645a6..0bad9a89 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs @@ -10,19 +10,19 @@ namespace PepperDash.Essentials.AppServer.Messengers /// Facilitates communication of device information by providing mechanisms for status updates and device /// information reporting. /// - /// The class integrates with an The class integrates with an to manage device-specific information. It uses a debounce timer to limit the /// frequency of updates, ensuring efficient communication. The timer is initialized with a 1-second interval and /// is disabled by default. This class also subscribes to device information change events and provides actions for /// reporting full device status and triggering updates. - public class DeviceInfoMessenger : MessengerBase + public class IDeviceInfoProviderMessenger : MessengerBase { private readonly IDeviceInfoProvider _deviceInfoProvider; private readonly Timer debounceTimer; /// - /// Initializes a new instance of the class, which facilitates communication + /// Initializes a new instance of the class, which facilitates communication /// of device information. /// /// The messenger uses a debounce timer to limit the frequency of certain operations. The @@ -30,7 +30,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// A unique identifier for the messenger instance. /// The path used for sending and receiving messages. /// An implementation of that provides device-specific information. - public DeviceInfoMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device) + public IDeviceInfoProviderMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device) { _deviceInfoProvider = device; diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index cba10266..1b0580f6 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -774,7 +774,7 @@ namespace PepperDash.Essentials { this.LogVerbose("Adding CurrentSourcesMessenger for {deviceKey}", device.Key); - var messenger = new CurrentSourcesMessenger($"{device.Key}-currentSources-{Key}", $"/device/{device.Key}", currentSources); + var messenger = new ICurrentSourcesMessenger($"{device.Key}-currentSources-{Key}", $"/device/{device.Key}", currentSources); AddDefaultDeviceMessenger(messenger); @@ -803,7 +803,7 @@ namespace PepperDash.Essentials this.LogVerbose("Adding IHasDeviceInfoMessenger for {deviceKey}", device.Key ); - var messenger = new DeviceInfoMessenger( + var messenger = new IDeviceInfoProviderMessenger( $"{device.Key}-deviceInfo-{Key}", $"/device/{device.Key}", provider diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 52a47273..01859cb2 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -140,6 +140,17 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListTieLines, + "listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes", + "Visualizes routes by signal type", + ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes", + "Visualizes current active routes from DefaultCollection", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => { foreach (var tl in TieLineCollection.Default) @@ -480,6 +491,282 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded."); + Extensions.MapDestinationsToSources(); + + Debug.LogMessage(LogEventLevel.Information, "All Routes Mapped."); + } + + + + /// + /// Visualizes routes in a tree format for better understanding of signal paths + /// + private void ListTieLines(string args) + { + try + { + if (args.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse("Usage: listtielines [signaltype]\r\n"); + CrestronConsole.ConsoleCommandResponse("Signal types: Audio, Video, SecondaryAudio, AudioVideo, UsbInput, UsbOutput\r\n"); + return; + } + + eRoutingSignalType? signalTypeFilter = null; + if (!string.IsNullOrEmpty(args)) + { + eRoutingSignalType parsedType; + if (Enum.TryParse(args.Trim(), true, out parsedType)) + { + signalTypeFilter = parsedType; + } + else + { + CrestronConsole.ConsoleCommandResponse("Invalid signal type: {0}\r\n", args.Trim()); + CrestronConsole.ConsoleCommandResponse("Valid types: Audio, Video, SecondaryAudio, AudioVideo, UsbInput, UsbOutput\r\n"); + return; + } + } + + var tielines = signalTypeFilter.HasValue + ? TieLineCollection.Default.Where(tl => tl.Type.HasFlag(signalTypeFilter.Value)) + : TieLineCollection.Default; + + var count = 0; + foreach (var tl in tielines) + { + CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); + count++; + } + + CrestronConsole.ConsoleCommandResponse("\r\nTotal: {0} tieline{1}{2}", count, count == 1 ? "" : "s", CrestronEnvironment.NewLine); + } + catch (Exception ex) + { + CrestronConsole.ConsoleCommandResponse("Error listing tielines: {0}\r\n", ex.Message); + } + } + + private void VisualizeRoutes(string args) + { + try + { + if (args.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse("Usage: visualizeroutes [signaltype] [-s source] [-d destination]\r\n"); + CrestronConsole.ConsoleCommandResponse(" signaltype: Audio, Video, AudioVideo, etc.\r\n"); + CrestronConsole.ConsoleCommandResponse(" -s: Filter by source key (partial match)\r\n"); + CrestronConsole.ConsoleCommandResponse(" -d: Filter by destination key (partial match)\r\n"); + return; + } + + ParseRouteFilters(args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter); + + CrestronConsole.ConsoleCommandResponse("\r\n+===========================================================================+\r\n"); + CrestronConsole.ConsoleCommandResponse("| ROUTE VISUALIZATION |\r\n"); + CrestronConsole.ConsoleCommandResponse("+===========================================================================+\r\n\r\n"); + + foreach (var descriptorCollection in Extensions.RouteDescriptors.Where(kv => kv.Value.Descriptors.Count() > 0)) + { + // Filter by signal type if specified + if (signalTypeFilter.HasValue && descriptorCollection.Key != signalTypeFilter.Value) + continue; + + CrestronConsole.ConsoleCommandResponse("\r\n+--- Signal Type: {0} ({1} routes) ---\r\n", + descriptorCollection.Key, + descriptorCollection.Value.Descriptors.Count()); + + foreach (var descriptor in descriptorCollection.Value.Descriptors) + { + // Filter by source/dest if specified + if (sourceFilter != null && !descriptor.Source.Key.ToLower().Contains(sourceFilter)) + continue; + if (destFilter != null && !descriptor.Destination.Key.ToLower().Contains(destFilter)) + continue; + + VisualizeRouteDescriptor(descriptor); + } + } + + CrestronConsole.ConsoleCommandResponse("\r\n"); + } + catch (Exception ex) + { + CrestronConsole.ConsoleCommandResponse("Error visualizing routes: {0}\r\n", ex.Message); + } + } + + private void VisualizeCurrentRoutes(string args) + { + try + { + if (args.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse("Usage: visualizecurrentroutes [signaltype] [-s source] [-d destination]\r\n"); + CrestronConsole.ConsoleCommandResponse(" signaltype: Audio, Video, AudioVideo, etc.\r\n"); + CrestronConsole.ConsoleCommandResponse(" -s: Filter by source key (partial match)\r\n"); + CrestronConsole.ConsoleCommandResponse(" -d: Filter by destination key (partial match)\r\n"); + return; + } + + ParseRouteFilters(args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter); + + CrestronConsole.ConsoleCommandResponse("\r\n+===========================================================================+\r\n"); + CrestronConsole.ConsoleCommandResponse("| CURRENT ROUTES VISUALIZATION |\r\n"); + CrestronConsole.ConsoleCommandResponse("+===========================================================================+\r\n\r\n"); + + var hasRoutes = false; + + // Get all descriptors from DefaultCollection + var allDescriptors = RouteDescriptorCollection.DefaultCollection.Descriptors; + + // Group by signal type + var groupedByType = allDescriptors.GroupBy(d => d.SignalType); + + foreach (var group in groupedByType) + { + var signalType = group.Key; + + // Filter by signal type if specified + if (signalTypeFilter.HasValue && signalType != signalTypeFilter.Value) + continue; + + var filteredDescriptors = group.Where(d => + { + if (sourceFilter != null && !d.Source.Key.ToLower().Contains(sourceFilter)) + return false; + if (destFilter != null && !d.Destination.Key.ToLower().Contains(destFilter)) + return false; + return true; + }).ToList(); + + if (filteredDescriptors.Count == 0) + continue; + + hasRoutes = true; + CrestronConsole.ConsoleCommandResponse("\r\n+--- Signal Type: {0} ({1} routes) ---\r\n", + signalType, + filteredDescriptors.Count); + + foreach (var descriptor in filteredDescriptors) + { + VisualizeRouteDescriptor(descriptor); + } + } + + if (!hasRoutes) + { + CrestronConsole.ConsoleCommandResponse("\r\nNo active routes found in current state.\r\n"); + } + + CrestronConsole.ConsoleCommandResponse("\r\n"); + } + catch (Exception ex) + { + CrestronConsole.ConsoleCommandResponse("Error visualizing current state: {0}\r\n", ex.Message); + } + } + + /// + /// Parses route filter arguments from command line + /// + /// Command line arguments + /// Parsed signal type filter (if any) + /// Parsed source filter (if any) + /// Parsed destination filter (if any) + private void ParseRouteFilters(string args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter) + { + signalTypeFilter = null; + sourceFilter = null; + destFilter = null; + + if (string.IsNullOrEmpty(args)) + return; + + var parts = args.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < parts.Length; i++) + { + var part = parts[i]; + + // Check for flags + if (part == "-s" && i + 1 < parts.Length) + { + sourceFilter = parts[++i].ToLower(); + } + else if (part == "-d" && i + 1 < parts.Length) + { + destFilter = parts[++i].ToLower(); + } + // Try to parse as signal type if not a flag and no signal type set yet + else if (!part.StartsWith("-") && !signalTypeFilter.HasValue) + { + if (Enum.TryParse(part, true, out eRoutingSignalType parsedType)) + { + signalTypeFilter = parsedType; + } + } + } + } + + /// + /// Visualizes a single route descriptor in a tree format + /// + private void VisualizeRouteDescriptor(RouteDescriptor descriptor) + { + CrestronConsole.ConsoleCommandResponse("|\r\n"); + CrestronConsole.ConsoleCommandResponse("|-- {0} --> {1}\r\n", + descriptor.Source.Key, + descriptor.Destination.Key); + + if (descriptor.Routes == null || descriptor.Routes.Count == 0) + { + CrestronConsole.ConsoleCommandResponse("| +-- (No switching steps)\r\n"); + return; + } + + for (int i = 0; i < descriptor.Routes.Count; i++) + { + var route = descriptor.Routes[i]; + var isLast = i == descriptor.Routes.Count - 1; + var prefix = isLast ? "+" : "|"; + var continuation = isLast ? " " : "|"; + + if (route.SwitchingDevice != null) + { + CrestronConsole.ConsoleCommandResponse("| {0}-- [{1}] {2}\r\n", + prefix, + route.SwitchingDevice.Key, + GetSwitchDescription(route)); + + // Add visual connection line for non-last items + if (!isLast) + CrestronConsole.ConsoleCommandResponse("| {0} |\r\n", continuation); + } + else + { + CrestronConsole.ConsoleCommandResponse("| {0}-- {1}\r\n", prefix, route.ToString()); + } + } + } + + /// + /// Gets a readable description of the switching operation + /// + private string GetSwitchDescription(RouteSwitchDescriptor route) + { + if (route.OutputPort != null && route.InputPort != null) + { + return string.Format("{0} -> {1}", route.OutputPort.Key, route.InputPort.Key); + } + else if (route.InputPort != null) + { + return string.Format("-> {0}", route.InputPort.Key); + } + else + { + return "(passthrough)"; + } } /// From 554916a74a72b242ddedba11499a73fda721a5f7 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Apr 2026 20:57:10 -0600 Subject: [PATCH 045/161] refactor: Remove commented generic interface from IRouting for clarity --- src/PepperDash.Essentials.Core/Routing/IRouting.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/IRouting.cs b/src/PepperDash.Essentials.Core/Routing/IRouting.cs index dd8ed041..e8ab124f 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRouting.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRouting.cs @@ -19,8 +19,3 @@ public interface IRouting : IRoutingInputsOutputs /// type of signal void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType); } - -/*public interface IRouting : IRoutingInputsOutputs -{ - void ExecuteSwitch(TInputSelector inputSelector, TOutputSelector outputSelector, eRoutingSignalType signalType); -}*/ \ No newline at end of file From ac3b73cf3254ce3355d451a5feac94911070917b Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Apr 2026 20:57:15 -0600 Subject: [PATCH 046/161] feat: Implement initialization exceptions handling and add API endpoint for retrieval --- .../IInitializationExceptions.cs | 16 ++++++ .../Web/EssentialsWebApi.cs | 8 ++- ...etInitializationExceptionRequestHandler.cs | 50 +++++++++++++++++++ src/PepperDash.Essentials/ControlSystem.cs | 31 ++++++++++-- 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IInitializationExceptions.cs create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/GetInitializationExceptionRequestHandler.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IInitializationExceptions.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IInitializationExceptions.cs new file mode 100644 index 00000000..55ce51ab --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IInitializationExceptions.cs @@ -0,0 +1,16 @@ + + +using System.Collections.Generic; + +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Defines the contract for devices that can report initialization exceptions. +/// +public interface IInitializationExceptions +{ + /// + /// Gets a list of exceptions that occurred during the initialization of the program. + /// + List InitializationExceptions { get; } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index f661a7ff..a1b81630 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -184,6 +184,11 @@ public class EssentialsWebApi : EssentialsDevice Name = "Get Routing Devices and TieLines", RouteHandler = new GetRoutingDevicesAndTieLinesHandler() }, + new HttpCwsRoute("initializationExceptions") + { + Name = "Get Initialization Exceptions", + RouteHandler = new GetInitializationExceptionsRequestHandler() + } }; AddRoute(routes); @@ -296,4 +301,5 @@ public class EssentialsWebApi : EssentialsDevice Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50)); } -} \ No newline at end of file +} + diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetInitializationExceptionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetInitializationExceptionRequestHandler.cs new file mode 100644 index 00000000..c0d77e41 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetInitializationExceptionRequestHandler.cs @@ -0,0 +1,50 @@ + +using Crestron.SimplSharp.WebScripting; +using System.Collections.Generic; +using System.Text; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Core.Web.RequestHandlers; +using System.Linq; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Request handler for retrieving initialization exceptions from the control system. +/// +public class GetInitializationExceptionsRequestHandler : WebApiBaseRequestHandler +{ + + /// + /// Initializes a new instance of the class. + /// + public GetInitializationExceptionsRequestHandler() : base(true) { } + + /// + protected override void HandleGet(HttpCwsContext context) + { + var exceptions = (Global.ControlSystem as IInitializationExceptions)?.InitializationExceptions ?? new List(); + + var response = Newtonsoft.Json.JsonConvert.SerializeObject(new + { + Exceptions = exceptions.Select(e => new + { + Message = e.Message, + StackTrace = e.StackTrace, + Type = e.GetType().FullName, + InnerException = e.InnerException != null ? new + { + Message = e.InnerException.Message, + StackTrace = e.InnerException.StackTrace, + Type = e.InnerException.GetType().FullName + } : null + }).ToList() + }); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(response, false); + context.Response.End(); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 01859cb2..5a6b8777 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -18,6 +18,8 @@ using Timeout = Crestron.SimplSharp.Timeout; using Serilog.Events; using System.Threading.Tasks; using PepperDash.Essentials.Core.Web; +using System.Collections.Generic; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; namespace PepperDash.Essentials; @@ -28,12 +30,19 @@ namespace PepperDash.Essentials; /// This class extends and serves as the entry point for the control /// system. It manages the initialization of devices, rooms, tie lines, and other system components. Additionally, it /// provides methods for platform determination, configuration loading, and system teardown. -public class ControlSystem : CrestronControlSystem, ILoadConfig +public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitializationExceptions { private Timer startTimer; private ManualResetEventSlim initializeEvent; private const long StartupTime = 500; + /// + /// List of exceptions that occurred during initialization. + /// This can be used to report issues with loading devices or tie lines without crashing the entire system, + /// which allows for partial functionality in cases where some components are misconfigured or have issues. + /// + public List InitializationExceptions { get; private set; } = new List(); + /// /// Initializes a new instance of the class, setting up the system's global state and /// dependencies. @@ -457,6 +466,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig } catch (Exception e) { + InitializationExceptions.Add(e); Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); } } @@ -482,12 +492,22 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig return; } - foreach (var tieLineConfig in ConfigReader.ConfigObject.TieLines) + try { - var newTL = tieLineConfig.GetTieLine(); - if (newTL != null) - tlc.Add(newTL); + + foreach (var tieLineConfig in ConfigReader.ConfigObject.TieLines) + { + var newTL = tieLineConfig.GetTieLine(); + if (newTL != null) + tlc.Add(newTL); + } } + catch (Exception e) + { + InitializationExceptions.Add(e); + Debug.LogMessage(e, "ERROR: Creating tie line. Skipping tie line."); + } + Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded."); @@ -796,6 +816,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig } catch (Exception ex) { + InitializationExceptions.Add(ex); Debug.LogMessage(ex, "Exception loading room {roomKey}:{roomType}", null, roomConfig.Key, roomConfig.Type); continue; } From 71cf7ef4dfb775ee969cdaf01bb5b6c80f417f19 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Apr 2026 21:01:22 -0600 Subject: [PATCH 047/161] feat: Add console command to report initialization exceptions for improved diagnostics --- src/PepperDash.Essentials/ControlSystem.cs | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 5a6b8777..388a9368 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -191,6 +191,9 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(PrintInitializationExceptions, + "getinitializationexceptions", "Reports any exceptions that occurred during initialization", ConsoleAccessLevelEnum.AccessOperator); + DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); if (!Debug.DoNotLoadConfigOnNextBoot) @@ -823,8 +826,28 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization } Debug.LogMessage(LogEventLevel.Information, "All Rooms Loaded."); + } + private void PrintInitializationExceptions(string args) + { + if(args.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse("Usage: getinitializationexceptions\r\n"); + CrestronConsole.ConsoleCommandResponse("Reports any exceptions that occurred during initialization.\r\n"); + return; + } + if (InitializationExceptions.Count == 0) + { + CrestronConsole.ConsoleCommandResponse("No initialization exceptions occurred.\r\n"); + return; + } + + CrestronConsole.ConsoleCommandResponse("Initialization Exceptions:\r\n"); + foreach (var ex in InitializationExceptions) + { + CrestronConsole.ConsoleCommandResponse(" - {0}: {1}\r\n", ex.GetType().FullName, ex.Message); + } } From 5f4a35d425e49f553d8c05fffe96424e3d08bdce Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Apr 2026 21:11:35 -0600 Subject: [PATCH 048/161] feat: Enhance initialization error handling and add console command for reporting exceptions --- src/PepperDash.Essentials/ControlSystem.cs | 162 +++++++++++---------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 388a9368..0706efcd 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -127,84 +127,92 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization private void StartSystem(object preventInitialization) { - DeterminePlatform(); - - // Print .NET runtime version - Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); - - if (Debug.DoNotLoadConfigOnNextBoot) + try { - CrestronConsole.AddNewConsoleCommand(s => Task.Run(() => GoWithLoad()), "go", "Loads configuration file", + DeterminePlatform(); + + // Print .NET runtime version + Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); + + if (Debug.DoNotLoadConfigOnNextBoot) + { + CrestronConsole.AddNewConsoleCommand(s => Task.Run(() => GoWithLoad()), "go", "Loads configuration file", + ConsoleAccessLevelEnum.AccessOperator); + } + + CrestronConsole.AddNewConsoleCommand(PluginLoader.ReportAssemblyVersions, "reportversions", "Reports the versions of the loaded assemblies", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(Core.DeviceFactory.GetDeviceFactoryTypes, "gettypes", "Gets the device types that can be built. Accepts a filter string.", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(BridgeHelper.PrintJoinMap, "getjoinmap", "map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(BridgeHelper.JoinmapMarkdown, "getjoinmapmarkdown" + , "generate markdown of map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(ListTieLines, + "listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes", + "Visualizes routes by signal type", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes", + "Visualizes current active routes from DefaultCollection", + ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => + { + foreach (var tl in TieLineCollection.Default) + CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); + }, + "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => + { + 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)); + }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => + CrestronConsole.ConsoleCommandResponse( + "This system can be found at the following URLs:{2}" + + "System URL: {0}{2}" + + "Template URL: {1}{2}", + ConfigReader.ConfigObject.SystemUrl, + ConfigReader.ConfigObject.TemplateUrl, + CrestronEnvironment.NewLine), + "portalinfo", + "Shows portal URLS from configuration", + ConsoleAccessLevelEnum.AccessOperator); + + + CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, + "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(PrintInitializationExceptions, + "getinitexceptions", "Reports any exceptions that occurred during initialization", ConsoleAccessLevelEnum.AccessOperator); + + DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); + + if (!Debug.DoNotLoadConfigOnNextBoot) + { + GoWithLoad(); + return; + } + + if (!(bool)preventInitialization) + { + SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; + } } - - CrestronConsole.AddNewConsoleCommand(PluginLoader.ReportAssemblyVersions, "reportversions", "Reports the versions of the loaded assemblies", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(Core.DeviceFactory.GetDeviceFactoryTypes, "gettypes", "Gets the device types that can be built. Accepts a filter string.", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(BridgeHelper.PrintJoinMap, "getjoinmap", "map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(BridgeHelper.JoinmapMarkdown, "getjoinmapmarkdown" - , "generate markdown of map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(ListTieLines, - "listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes", - "Visualizes routes by signal type", - ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes", - "Visualizes current active routes from DefaultCollection", - ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => + catch (Exception e) { - foreach (var tl in TieLineCollection.Default) - CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); - }, - "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => - { - 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)); - }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => - CrestronConsole.ConsoleCommandResponse( - "This system can be found at the following URLs:{2}" + - "System URL: {0}{2}" + - "Template URL: {1}{2}", - ConfigReader.ConfigObject.SystemUrl, - ConfigReader.ConfigObject.TemplateUrl, - CrestronEnvironment.NewLine), - "portalinfo", - "Shows portal URLS from configuration", - ConsoleAccessLevelEnum.AccessOperator); - - - CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, - "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(PrintInitializationExceptions, - "getinitializationexceptions", "Reports any exceptions that occurred during initialization", ConsoleAccessLevelEnum.AccessOperator); - - DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); - - if (!Debug.DoNotLoadConfigOnNextBoot) - { - GoWithLoad(); - return; - } - - if (!(bool)preventInitialization) - { - SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; + InitializationExceptions.Add(e); + Debug.LogFatal(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); } } @@ -277,6 +285,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization } catch (Exception e) { + InitializationExceptions.Add(e); Debug.LogMessage(e, "Unable to determine platform due to exception"); } } @@ -331,7 +340,8 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization } catch (Exception e) { - Debug.LogMessage(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); + InitializationExceptions.Add(e); + Debug.LogFatal(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); } finally { @@ -830,7 +840,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization private void PrintInitializationExceptions(string args) { - if(args.Contains("?")) + if (args.Contains("?")) { CrestronConsole.ConsoleCommandResponse("Usage: getinitializationexceptions\r\n"); CrestronConsole.ConsoleCommandResponse("Reports any exceptions that occurred during initialization.\r\n"); From 7d60c41dbcb506a8265ae296bc935f942e67b693 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Apr 2026 21:50:24 -0600 Subject: [PATCH 049/161] feat: Rename LoadConfig2 to LoadConfig and implement plugin version checks against configuration --- .../Config/Essentials/ConfigReader.cs | 2 +- .../Config/ILoadConfig.cs | 6 +++ src/PepperDash.Essentials/ControlSystem.cs | 53 ++++++++++++++++++- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index 362ab9f0..e402341e 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -35,7 +35,7 @@ namespace PepperDash.Essentials.Core.Config; /// /// LoadConfig2 method /// - public static bool LoadConfig2() + public static bool LoadConfig() { Debug.LogMessage(LogEventLevel.Information, "Loading unmerged system/template portal configuration file."); try diff --git a/src/PepperDash.Essentials.Core/Config/ILoadConfig.cs b/src/PepperDash.Essentials.Core/Config/ILoadConfig.cs index a02baa8c..8c093176 100644 --- a/src/PepperDash.Essentials.Core/Config/ILoadConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/ILoadConfig.cs @@ -6,7 +6,13 @@ using System.Threading.Tasks; namespace PepperDash.Essentials.Core; +/// +/// Defines the contract for loading configuration. This is called after all plugins have been loaded, but before any devices are initialized. This allows for any necessary configuration merging or processing to be done before devices are created and initialized. +/// public interface ILoadConfig { + /// + /// Executes the loading of the configuration. This is called after all plugins have been loaded, but before any devices are initialized. This allows for any necessary configuration merging or processing to be done before devices are created and initialized. + /// void GoWithLoad(); } diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 0706efcd..a769d2c6 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -314,12 +314,14 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization PluginLoader.LoadPlugins(); Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config..."); - if (!ConfigReader.LoadConfig2() || ConfigReader.ConfigObject == null) + if (!ConfigReader.LoadConfig() || ConfigReader.ConfigObject == null) { Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file. Please ensure a valid config file is present and restart the program."); // return; } + CheckPluginVersionsAgainstConfig(); + Load(); Debug.LogMessage(LogEventLevel.Information, "Essentials load complete"); } @@ -352,6 +354,55 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization } + private void CheckPluginVersionsAgainstConfig() + { + var versions = ConfigReader.ConfigObject.Versions; + + if (versions != null) + { + var pluginVersions = PluginLoader.EssentialsPluginAssemblies + .Select(a => + { + var packageId = a.GetCustomAttributes() + .FirstOrDefault(attr => attr.Key == "NuGetPackageId")?.Value + ?? a.Name; + return (packageId, version: a.Version.ToString()); + }) + .ToDictionary(x => x.packageId, x => x.version); + + if (versions.Essentials != null) + { + if (pluginVersions.TryGetValue("PepperDashEssentials", out var pluginVersion)) + { + if (pluginVersion != versions.Essentials) + { + Debug.LogMessage(LogEventLevel.Warning, + "Essentials version mismatch. Config version: {configVersion:l}, Loaded plugin version: {pluginVersion:l}", + versions.Essentials, pluginVersion); + } + } + } + + + foreach (var version in versions.Packages) + { + if (pluginVersions.TryGetValue(version.PackageId, out var pluginVersion)) + { + if (pluginVersion != version.Version) + { + Debug.LogMessage(LogEventLevel.Warning, + "Plugin version mismatch for {pluginKey:l}. Config version: {configVersion:l}, Loaded plugin version: {pluginVersion:l}", + version.PackageId, version.Version, pluginVersion); + } + } + else + { + Debug.LogMessage(LogEventLevel.Warning, + "Plugin {pluginKey:l} specified in config versions but not found in loaded plugins.", version.PackageId); + } + } + } + } /// /// Verifies filesystem is set up. IR, SGD, and programX folders From af7411f43b64cbb50aaf89acfc628a02de577ded Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Apr 2026 22:03:07 -0600 Subject: [PATCH 050/161] fix: Correctly retrieve package ID from assembly metadata for version checks --- src/PepperDash.Essentials/ControlSystem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index a769d2c6..e89d3084 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -363,7 +363,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization var pluginVersions = PluginLoader.EssentialsPluginAssemblies .Select(a => { - var packageId = a.GetCustomAttributes() + var packageId = a.Assembly.GetCustomAttributes() .FirstOrDefault(attr => attr.Key == "NuGetPackageId")?.Value ?? a.Name; return (packageId, version: a.Version.ToString()); @@ -374,7 +374,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization { if (pluginVersions.TryGetValue("PepperDashEssentials", out var pluginVersion)) { - if (pluginVersion != versions.Essentials) + if (pluginVersion != versions.Essentials.PackageId) { Debug.LogMessage(LogEventLevel.Warning, "Essentials version mismatch. Config version: {configVersion:l}, Loaded plugin version: {pluginVersion:l}", From d1501d2dab280c4524dd53c99f5866a01c81a5e1 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Apr 2026 22:35:47 -0600 Subject: [PATCH 051/161] feat: Enhance plugin version check with error handling and logging --- src/PepperDash.Essentials/ControlSystem.cs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index e89d3084..4d031cec 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -356,10 +356,18 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization private void CheckPluginVersionsAgainstConfig() { - var versions = ConfigReader.ConfigObject.Versions; - - if (versions != null) + try { + Debug.LogInformation("Checking plugin versions against config..."); + + if (ConfigReader.ConfigObject == null) + return; + + var versions = ConfigReader.ConfigObject.Versions; + + if (versions == null) + return; + var pluginVersions = PluginLoader.EssentialsPluginAssemblies .Select(a => { @@ -383,6 +391,8 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization } } + if (versions.Packages == null) + return; foreach (var version in versions.Packages) { @@ -402,6 +412,11 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization } } } + catch (Exception ex) + { + InitializationExceptions.Add(ex); + Debug.LogMessage(ex, "Error checking plugin versions against config. Continuing with load."); + } } /// From dc656bddf120368fe9d937805c452551dc8c286e Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 16 Apr 2026 21:31:42 -0600 Subject: [PATCH 052/161] feat: Implement login request handler with token generation and error handling --- src/PepperDash.Core/Web/WebApiServer.cs | 2 + .../Web/EssentialsWebApi.cs | 6 + .../Web/EssentialsWebApiHelpers.cs | 8 +- .../GetRoutingDevicesAndTieLinesHandler.cs | 110 +++++++++++++++- .../RequestHandlers/LoginRequestHandler.cs | 121 ++++++++++++++++++ 5 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs index bccd52aa..814a6f1d 100644 --- a/src/PepperDash.Core/Web/WebApiServer.cs +++ b/src/PepperDash.Core/Web/WebApiServer.cs @@ -75,6 +75,8 @@ public class WebApiServer : IKeyName if (_server == null) _server = new HttpCwsServer(BasePath); + _server.AuthenticateAllRoutes = false; + _server.setProcessName(Key); _server.HttpRequestHandler = new DefaultRequestHandler(); _server.ReceivedRequestEvent += ReceivedRequestEventHandler; diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index a1b81630..eaf3bab8 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -69,6 +69,7 @@ public class EssentialsWebApi : EssentialsDevice _server = new WebApiServer(Key, Name, BasePath); + _debugServer = new WebApiServer($"{key}-debug-app", $"{name} Debug App", "/debug"); _debugServer.SetFallbackHandler(new ServeDebugAppRequestHandler()); @@ -79,6 +80,11 @@ public class EssentialsWebApi : EssentialsDevice { var routes = new List { + new HttpCwsRoute("login") + { + Name = "Root", + RouteHandler = new LoginRequestHandler() + }, new HttpCwsRoute("versions") { Name = "ReportVersions", diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs index 243667c0..bb7e8502 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs @@ -40,12 +40,18 @@ public static class EssentialsWebApiHelpers /// public static object MapToDeviceListObject(IKeyed device) { + var interfaces = device.GetType() + .GetInterfaces() + .Select(i => i.Name) + .ToList(); + return new { device.Key, Name = (device is IKeyName) ? (device as IKeyName).Name - : "---" + : "---", + Interfaces = interfaces }; } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs index 72a6757f..f924c951 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs @@ -40,26 +40,26 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers if (device is IRoutingInputs inputDevice) { deviceInfo.HasInputs = true; - deviceInfo.InputPorts = inputDevice.InputPorts.Select(p => new PortInfo + deviceInfo.InputPorts = [.. inputDevice.InputPorts.Select(p => new PortInfo { Key = p.Key, SignalType = p.Type.ToString(), ConnectionType = p.ConnectionType.ToString(), IsInternal = p.IsInternal - }).ToList(); + })]; } // Check if device implements IRoutingOutputs if (device is IRoutingOutputs outputDevice) { deviceInfo.HasOutputs = true; - deviceInfo.OutputPorts = outputDevice.OutputPorts.Select(p => new PortInfo + deviceInfo.OutputPorts = [.. outputDevice.OutputPorts.Select(p => new PortInfo { Key = p.Key, SignalType = p.Type.ToString(), ConnectionType = p.ConnectionType.ToString(), IsInternal = p.IsInternal - }).ToList(); + })]; } // Check if device implements IRoutingInputsOutputs @@ -86,10 +86,31 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers IsInternal = tl.IsInternal }).ToList(); + // Get current active routes from DefaultCollection, grouped by signal type + var currentRoutes = RouteDescriptorCollection.DefaultCollection.Descriptors + .GroupBy(d => d.SignalType.ToString()) + .Select(g => new CurrentRouteGroupInfo + { + SignalType = g.Key, + Routes = [.. g.Select(d => new ActiveRouteInfo + { + SourceDeviceKey = d.Source.Key, + DestinationDeviceKey = d.Destination.Key, + DestinationInputPortKey = d.InputPort?.Key, + Steps = [.. d.Routes.Select(r => new RouteSwitchStepInfo + { + SwitchingDeviceKey = r.SwitchingDevice?.Key, + InputPortKey = r.InputPort?.Key, + OutputPortKey = r.OutputPort?.Key + })] + })] + }).ToList(); + var response = new RoutingSystemInfo { Devices = devices, - TieLines = tielines + TieLines = tielines, + CurrentRoutes = currentRoutes }; var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); @@ -121,6 +142,12 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// [JsonProperty("tieLines")] public List TieLines { get; set; } + + /// + /// Gets or sets the current active routes in the system, grouped by signal type + /// + [JsonProperty("currentRoutes")] + public List CurrentRoutes { get; set; } } /// @@ -238,4 +265,77 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers [JsonProperty("isInternal")] public bool IsInternal { get; set; } } + + /// + /// Represents a group of active routes for a given signal type + /// + public class CurrentRouteGroupInfo + { + /// + /// Gets or sets the signal type for the group of active routes (e.g., AudioVideo, Audio, Video, etc.) + /// + [JsonProperty("signalType")] + public string SignalType { get; set; } + + /// + /// Gets or sets the list of active routes for the given signal type + /// + [JsonProperty("routes")] + public List Routes { get; set; } + } + + /// + /// Represents a single active route from a source to a destination + /// + public class ActiveRouteInfo + { + /// + /// Gets or sets the key of the source device for the active route + /// + [JsonProperty("sourceDeviceKey")] + public string SourceDeviceKey { get; set; } + + /// + /// Gets or sets the key of the destination device for the active route + /// + [JsonProperty("destinationDeviceKey")] + public string DestinationDeviceKey { get; set; } + + /// + /// Gets or sets the key of the destination input port for the active route, if applicable + /// + [JsonProperty("destinationInputPortKey", NullValueHandling = NullValueHandling.Ignore)] + public string DestinationInputPortKey { get; set; } + + /// + /// Gets or sets the list of switching steps for the active route + /// + [JsonProperty("steps")] + public List Steps { get; set; } + } + + /// + /// Represents a single switching step within a route + /// + public class RouteSwitchStepInfo + { + /// + /// Gets or sets the key of the switching device for the route step + /// + [JsonProperty("switchingDeviceKey", NullValueHandling = NullValueHandling.Ignore)] + public string SwitchingDeviceKey { get; set; } + + + /// + /// Gets or sets the key of the input port for the route step, if applicable + /// + [JsonProperty("inputPortKey", NullValueHandling = NullValueHandling.Ignore)] + public string InputPortKey { get; set; } + + /// + /// Gets or sets the key of the output port for the route step, if applicable + /// + [JsonProperty("outputPortKey", NullValueHandling = NullValueHandling.Ignore)] + public string OutputPortKey { get; set; } + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs new file mode 100644 index 00000000..18e62d63 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs @@ -0,0 +1,121 @@ + +using System; +using Crestron.SimplSharp.CrestronAuthentication; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core.Web.RequestHandlers; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Represents a LoginRequestHandler +/// +public class LoginRequestHandler : WebApiBaseRequestHandler +{ + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public LoginRequestHandler() + : base(true) + { + } + + /// + /// Handles POST method requests for user login and token generation + /// + /// The HTTP context for the request. + protected override void HandlePost(HttpCwsContext context) + { + try + { + if (context.Request.ContentLength < 0) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var data = context.Request.GetRequestBody(); + if (string.IsNullOrEmpty(data)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var loginRequest = JsonConvert.DeserializeObject(data); + + if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Username) || string.IsNullOrEmpty(loginRequest.Password)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + Authentication.UserToken token; + + try + { + token = Authentication.GetAuthenticationToken(loginRequest.Username, loginRequest.Password); + } + catch (ArgumentException) + { + context.Response.StatusCode = 401; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + if (!token.Valid) + { + context.Response.StatusCode = 401; + context.Response.StatusDescription = "Unauthorized"; + context.Response.End(); + + return; + } + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(JsonConvert.SerializeObject(new { Token = token }, Formatting.Indented), false); + context.Response.End(); + } + catch (System.Exception ex) + { + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(JsonConvert.SerializeObject(new { Error = ex.Message }, Formatting.Indented), false); + context.Response.End(); + } + } +} + +/// +/// Represents a LoginRequest +/// +public class LoginRequest +{ + /// + /// Gets or sets the username. + /// + public string Username { get; set; } + + /// + /// Gets or sets the password. + /// + public string Password { get; set; } +} From 89a1b662b046079f5357b70064427f8dd89e856a Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 24 Apr 2026 10:19:18 -0500 Subject: [PATCH 053/161] fix: return early if config devices are null, with an appropriate log message --- src/PepperDash.Essentials/ControlSystem.cs | 87 ++++++++++++---------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 4d031cec..a7ba6dc9 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -1,7 +1,10 @@ -using System; +using System; +using System.Collections.Generic; using System.IO.Compression; using System.Linq; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; @@ -12,14 +15,11 @@ using PepperDash.Core.Adapters; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; -using PepperDash.Essentials.Core.Routing; -using System.Threading; -using Timeout = Crestron.SimplSharp.Timeout; -using Serilog.Events; -using System.Threading.Tasks; -using PepperDash.Essentials.Core.Web; -using System.Collections.Generic; using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Core.Web; +using Serilog.Events; +using Timeout = Crestron.SimplSharp.Timeout; namespace PepperDash.Essentials; @@ -506,53 +506,58 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization new Core.Monitoring.SystemMonitorController("systemMonitor")); } - if (ConfigReader.ConfigObject is not null) + if (ConfigReader.ConfigObject is null) { Debug.LogMessage(LogEventLevel.Warning, "LoadDevices: ConfigObject is null. Cannot load devices."); + return; + } - foreach (var devConf in ConfigReader.ConfigObject.Devices) + Debug.LogInformation("Loading devices from config."); + + foreach (var devConf in ConfigReader.ConfigObject.Devices) + { + IKeyed newDev = null; + + try { - IKeyed newDev = null; - - try + Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type); + // Skip this to prevent unnecessary warnings + if (devConf.Key == "processor") { - Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type); - // Skip this to prevent unnecessary warnings - if (devConf.Key == "processor") - { - var prompt = Global.ControlSystem.ControllerPrompt; + var prompt = Global.ControlSystem.ControllerPrompt; - var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) || - string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase); + var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) || + string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase); - if (!typeMatch) - Debug.LogMessage(LogEventLevel.Information, - "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", - devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); + if (!typeMatch) + Debug.LogMessage(LogEventLevel.Information, + "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", + devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); - continue; - } - - - if (newDev == null) - newDev = Core.DeviceFactory.GetDevice(devConf); - - if (newDev != null) - DeviceManager.AddDevice(newDev); - else - Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); - } - catch (Exception e) - { - InitializationExceptions.Add(e); - Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); + continue; } + + + if (newDev == null) + newDev = Core.DeviceFactory.GetDevice(devConf); + + if (newDev != null) + DeviceManager.AddDevice(newDev); + else + Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); + } + catch (Exception e) + { + InitializationExceptions.Add(e); + Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); } - } Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded."); + return; + + } From 2e1bb8b28645f0dccaee11394a5fb43ec47c8401 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 1 May 2026 14:44:07 -0600 Subject: [PATCH 054/161] feat: Enhance WebSocket URL generation to support dual-stack environments by using CSLAN IP if available --- src/PepperDash.Core/Logging/DebugWebsocketSink.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 52d61896..6214505b 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -61,7 +61,12 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed if (_httpsServer == null || !_httpsServer.IsListening) return ""; var service = _httpsServer.WebSocketServices[_path]; if (service == null) return ""; - return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}"; + + // Use CSLAN IP if available, otherwise fallback to primary IP. This ensures we provide a reachable URL in dual-stack environments. + if (!string.IsNullOrEmpty(CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1))) + return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1)}:{_httpsServer.Port}{service.Path}"; + else + return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}"; } } From bc70aa629c2be4157307773f53aa79b395fcbe19 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 6 May 2026 13:44:09 -0600 Subject: [PATCH 055/161] Enhance logging and mobile control functionality - Added error log level change notification in Debug.cs. - Updated appdebug command usage to include sink options and timeout parameters in Debug.cs. - Implemented AddDefaultMessengersForDevice method in IMobileControl interface for better messenger registration. - Modified EssentialsDevice to create mobile control messengers based on available interfaces. - Introduced MessengerFactoryRegistry to streamline messenger creation for various device types. - Refactored MobileControlSystemController to utilize the new messenger registration system and removed redundant setup methods. Co-authored-by: Copilot --- src/PepperDash.Core/Logging/Debug.cs | 98 ++- .../DeviceTypeInterfaces/IMobileControl.cs | 17 + .../Devices/EssentialsDevice.cs | 21 +- .../MessengerFactoryRegistry.cs | 323 +++++++ .../MobileControlSystemController.cs | 785 +----------------- 5 files changed, 445 insertions(+), 799 deletions(-) create mode 100644 src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 97a6e93e..4160da21 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -272,6 +272,11 @@ public static class Debug { LogMessage(LogEventLevel.Information, "Console debug level set to {minimumLevel}", consoleLoggingLevelSwitch.MinimumLevel); }; + + errorLogLevelSwitch.MinimumLevelChanged += (sender, args) => + { + LogMessage(LogEventLevel.Information, "Error log debug level set to {minimumLevel}", errorLogLevelSwitch.MinimumLevel); + }; } catch (Exception ex) { @@ -424,61 +429,88 @@ public static class Debug if (levelString.Trim() == "?") { _console?.ConsoleCommandResponse( - - "Used to set the minimum level of debug messages to be printed to the console:\r\n" + - "[LogLevel] [TimeoutInMinutes]\r\n" + - "If TimeoutInMinutes is not provided, it will default to 120 minutes. If provided, the level will reset to Information after the timeout period elapses.\r\n" + - "LogLevel can be either a number from 0-5 or a log level name. If using a number, the mapping is as follows:\r\n" + - $"{_logLevels[0]} = 0\r\n" + - $"{_logLevels[1]} = 1\r\n" + - $"{_logLevels[2]} = 2\r\n" + - $"{_logLevels[3]} = 3\r\n" + - $"{_logLevels[4]} = 4\r\n" + - $"{_logLevels[5]} = 5"); + "Used to set the minimum level of debug messages:\r\n" + + "Usage: appdebug:P [sink] [level] [timeoutMinutes]\r\n" + + " sink (optional): console (default), errorlog, websocket, file, all\r\n" + + " timeoutMinutes (optional, console sink only): level resets to Information after this many minutes (default 120)\r\n" + + $" level: {string.Join(", ", _logLevels.Values)}"); return; } if (string.IsNullOrEmpty(levelString.Trim())) { - _console?.ConsoleCommandResponse($"AppDebug level = {consoleLoggingLevelSwitch.MinimumLevel}"); + _console?.ConsoleCommandResponse($"Console log level = {consoleLoggingLevelSwitch.MinimumLevel}\r\n"); + _console?.ConsoleCommandResponse($"File log level = {fileLoggingLevelSwitch.MinimumLevel}\r\n"); + _console?.ConsoleCommandResponse($"Error log level = {errorLogLevelSwitch.MinimumLevel}\r\n"); + _console?.ConsoleCommandResponse($"Websocket log level = {websocketLoggingLevelSwitch.MinimumLevel}\r\n"); return; } - // split on space to allow for potential future addition of timeout parameter without breaking existing command usage - var parts = Regex.Split(levelString.Trim(), @"\s+"); - levelString = parts[0]; + var tokens = Regex.Split(levelString.Trim(), @"\s+"); - if (parts.Length > 1 && long.TryParse(parts[1], out var timeout)) + // Determine whether the first token is a sink name or a level + var knownSinks = new[] { "console", "errorlog", "websocket", "file", "all" }; + string sinkName; + string levelToken; + long timeoutMinutes = defaultConsoleDebugTimeoutMin; + + if (tokens.Length >= 2 && Array.IndexOf(knownSinks, tokens[0].ToLowerInvariant()) >= 0) { - timeout = Math.Max(timeout, 1); // enforce minimum timeout of 1 minute - consoleDebugTimer.Interval = timeout * 60000; + // First token is a sink name + sinkName = tokens[0].ToLowerInvariant(); + levelToken = tokens[1]; + + if (tokens.Length >= 3 && long.TryParse(tokens[2], out var parsedTimeout)) + timeoutMinutes = Math.Max(parsedTimeout, 1); + } + else + { + // No sink prefix — treat as console with optional timeout as second token + sinkName = "console"; + levelToken = tokens[0]; + + if (tokens.Length >= 2 && long.TryParse(tokens[1], out var parsedTimeout)) + timeoutMinutes = Math.Max(parsedTimeout, 1); } - // first try to parse as int for backward compatibility with existing usage of numeric levels + // Parse the level + LogEventLevel level; - if (int.TryParse(levelString, out var levelInt)) + if (!Enum.TryParse(levelToken, true, out level)) { - if (levelInt < 0 || levelInt > 5) - { - _console?.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5"); - return; - } - SetDebugLevel((uint)levelInt); + _console?.ConsoleCommandResponse($"Error: Unable to parse '{levelToken}' to valid log level. Valid levels: {string.Join(", ", _logLevels.Values)}"); return; } - // make this parse attempt case-insensitive to allow for more flexible command usage - if (Enum.TryParse(levelString, true, out var levelEnum)) + // Apply to the specified sink(s) + switch (sinkName) { - SetDebugLevel(levelEnum); - return; + case "console": + SetDebugLevel(level, (int)timeoutMinutes); + break; + case "errorlog": + SetErrorLogMinimumDebugLevel(level); + break; + case "websocket": + SetWebSocketMinimumDebugLevel(level); + break; + case "file": + SetFileMinimumDebugLevel(level); + break; + case "all": + SetDebugLevel(level, (int)timeoutMinutes); + SetErrorLogMinimumDebugLevel(level); + SetWebSocketMinimumDebugLevel(level); + SetFileMinimumDebugLevel(level); + break; + default: + _console?.ConsoleCommandResponse($"Error: Unknown sink '{sinkName}'. Valid sinks: console, errorlog, websocket, file, all"); + break; } - - _console?.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level"); } catch { - _console?.ConsoleCommandResponse("Usage: appdebug:P [0-5]"); + _console?.ConsoleCommandResponse("Usage: appdebug:P [sink] [level] [timeoutMinutes]"); } } diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControl.cs index f920e5fe..074d51de 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControl.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Essentials.Core; namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; @@ -68,4 +70,19 @@ public interface IMobileControl : IKeyed /// messenger key to find /// Messenger if found, null otherwise IMobileControlRoomMessenger GetRoomMessenger(string key); + + /// + /// Registers default mobile-control messengers for the given device based on the + /// interfaces it implements. When is null all + /// applicable messengers are added; when a list is supplied only messengers whose + /// registry entry type appears in that list are added. + /// + /// The device to add messengers for. + /// + /// Optional filter. Pass typeof(IBasicVolumeControls), + /// typeof(IHasInputs<string>), etc. to restrict which messengers are + /// created. Pass null (or omit the argument) to add all applicable messengers. + /// + void AddDefaultMessengersForDevice(EssentialsDevice device, IEnumerable interfaces = null); + } diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs index 305a14ec..7a62d1c3 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using PepperDash.Core; using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; using Serilog.Events; namespace PepperDash.Essentials.Core; @@ -84,7 +86,10 @@ public abstract class EssentialsDevice : Device } /// - /// Override this method to perform any initialization that requires all devices to be activated. This method is called automatically after the DeviceManager.AllDevicesActivated event is fired, and should not be called directly. + /// Override this method to perform any initialization that requires all devices to be activated. + /// This method is called automatically after the DeviceManager.AllDevicesActivated event is fired, + /// and should not be called directly. + /// If this is not called then no Mobile Control messengers will be created. /// /// protected override bool CustomActivate() @@ -99,7 +104,21 @@ public abstract class EssentialsDevice : Device /// protected virtual void CreateMobileControlMessengers() { + var controller = DeviceManager.AllDevices + .OfType() + .FirstOrDefault(); + if (controller == null) + { + Debug.LogMessage( + LogEventLevel.Warning, + this, + "No IMobileControl controller found; default messengers will not be registered for {key}", + Key); + return; + } + + controller.AddDefaultMessengersForDevice(this); } } diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs new file mode 100644 index 00000000..e333cd0b --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.CrestronIO; +using PepperDash.Essentials.Core.DeviceInfo; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Lighting; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Core.Shades; +using PepperDash.Essentials.Devices.Common.AudioCodec; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Displays; +using PepperDash.Essentials.Devices.Common.SoftCodec; +using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Room.MobileControl; + +namespace PepperDash.Essentials +{ + /// + /// Describes a single entry in . + /// + internal sealed class MessengerFactoryEntry + { + /// + /// The primary type used for both IsAssignableFrom matching and for + /// caller-supplied type filtering via . + /// + public Type InterfaceType { get; } + + /// + /// Optional additional condition. When null the entry matches any device + /// whose runtime type satisfies . + /// + public Func Predicate { get; } + + /// + /// Factory that creates the messenger. Parameters are + /// (device, messagePath, controllerKey). + /// + public Func Factory { get; } + + public MessengerFactoryEntry( + Type interfaceType, + Func factory, + Func predicate = null) + { + InterfaceType = interfaceType; + Factory = factory; + Predicate = predicate; + } + + /// + /// Returns true when this entry should produce a messenger for + /// . + /// + public bool Matches(EssentialsDevice device) + { + return Predicate != null + ? Predicate(device) + : InterfaceType.IsAssignableFrom(device.GetType()); + } + } + + /// + /// Maps device interface / class types to messenger factories, replacing the + /// previous cascade of if (device is X) checks in + /// SetupDefaultDeviceMessengers. + /// + internal static class MessengerFactoryRegistry + { + public static IReadOnlyList Entries { get; } = + new List + { + // ── Communication ──────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(ICommunicationMonitor), + (d, mp, ck) => new ICommunicationMonitorMessenger( + $"{d.Key}-commMonitor-{ck}", mp, (ICommunicationMonitor)d) + ), + + // ── Cameras ────────────────────────────────────────────────────────────── + // CameraBase only when the device does NOT also implement IHasCameraControls + new MessengerFactoryEntry( + typeof(CameraBase), + (d, mp, ck) => new CameraBaseMessenger( + $"{d.Key}-cameraBase-{ck}", (CameraBase)d, mp), + predicate: d => d is CameraBase && !(d is IHasCameraControls) + ), + + new MessengerFactoryEntry( + typeof(IHasCameraControls), + (d, mp, ck) => new CameraBaseMessenger( + $"{d.Key}-hasCamerasWithControls-{ck}", (IHasCameraControls)d, mp) + ), + new MessengerFactoryEntry( + typeof(IHasCamerasWithControls), + (d, mp, ck) => new IHasCamerasWithControlMessenger( + $"{d.Key}-cameras-{ck}", mp, (IHasCamerasWithControls)d) + ), + + // ── Routing ────────────────────────────────────────────────────────────── + // BlueJeansPc implements IRunRouteAction + new MessengerFactoryEntry( + typeof(BlueJeansPc), + (d, mp, ck) => new RunRouteActionMessenger( + $"{d.Key}-runRouteAction-{ck}", (IRunRouteAction)d, mp) + ), + + // ── Presets ────────────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(ITvPresetsProvider), + (d, mp, ck) => new DevicePresetsModelMessenger( + $"{d.Key}-presets-{ck}", mp, (ITvPresetsProvider)d) + ), + + // ── Displays ───────────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(DisplayBase), + (d, mp, ck) => new DisplayBaseMessenger( + $"{d.Key}-displayBase-{ck}", mp, (DisplayBase)d) + ), + new MessengerFactoryEntry( + typeof(TwoWayDisplayBase), + (d, mp, ck) => new TwoWayDisplayBaseMessenger( + $"{d.Key}-twoWayDisplay-{ck}", mp, (TwoWayDisplayBase)d) + ), + + // ── Audio / Video ───────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IBasicVolumeControls), + (d, mp, ck) => new DeviceVolumeMessenger( + $"{d.Key}-volume-{ck}", mp, (IBasicVolumeControls)d) + ), + new MessengerFactoryEntry( + typeof(IBasicVideoMuteWithFeedback), + (d, mp, ck) => new IBasicVideoMuteWithFeedbackMessenger( + $"{d.Key}-videoMute-{ck}", mp, (IBasicVideoMuteWithFeedback)d) + ), + + // ── Lighting ───────────────────────────────────────────────────────────── + // ILightingScenes covers LightingBase too (LightingBase : ILightingScenes) + new MessengerFactoryEntry( + typeof(ILightingScenes), + (d, mp, ck) => new ILightingScenesMessenger( + $"{d.Key}-lighting-{ck}", (ILightingScenes)d, mp) + ), + + // ── Shades ─────────────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IShadesOpenCloseStop), + (d, mp, ck) => new IShadesOpenCloseStopMessenger( + $"{d.Key}-shades-{ck}", (IShadesOpenCloseStop)d, mp) + ), + + // ── Codecs ─────────────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(VideoCodecBase), + (d, mp, ck) => new VideoCodecBaseMessenger( + $"{d.Key}-videoCodec-{ck}", (VideoCodecBase)d, mp) + ), + new MessengerFactoryEntry( + typeof(AudioCodecBase), + (d, mp, ck) => new AudioCodecBaseMessenger( + $"{d.Key}-audioCodec-{ck}", (AudioCodecBase)d, mp) + ), + + // ── Set-top box controls ────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(ISetTopBoxControls), + (d, mp, ck) => new ISetTopBoxControlsMessenger( + $"{d.Key}-stb-{ck}", mp, (ISetTopBoxControls)d) + ), + new MessengerFactoryEntry( + typeof(IChannel), + (d, mp, ck) => new IChannelMessenger( + $"{d.Key}-channel-{ck}", mp, (IChannel)d) + ), + new MessengerFactoryEntry( + typeof(IColor), + (d, mp, ck) => new IColorMessenger( + $"{d.Key}-color-{ck}", mp, (IColor)d) + ), + new MessengerFactoryEntry( + typeof(IDPad), + (d, mp, ck) => new IDPadMessenger( + $"{d.Key}-dPad-{ck}", mp, (IDPad)d) + ), + new MessengerFactoryEntry( + typeof(INumericKeypad), + (d, mp, ck) => new INumericKeypadMessenger( + $"{d.Key}-numericKeypad-{ck}", mp, (INumericKeypad)d) + ), + + // ── Power ───────────────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IHasPowerControl), + (d, mp, ck) => new IHasPowerMessenger( + $"{d.Key}-powerControl-{ck}", mp, (IHasPowerControl)d) + ), + new MessengerFactoryEntry( + typeof(IHasPowerControlWithFeedback), + (d, mp, ck) => new IHasPowerControlWithFeedbackMessenger( + $"{d.Key}-powerFeedback-{ck}", mp, (IHasPowerControlWithFeedback)d) + ), + + // ── Transport / sources ─────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(ITransport), + (d, mp, ck) => new ITransportMessenger( + $"{d.Key}-transport-{ck}", mp, (ITransport)d) + ), + new MessengerFactoryEntry( + typeof(ICurrentSources), + (d, mp, ck) => new ICurrentSourcesMessenger( + $"{d.Key}-currentSources-{ck}", mp, (ICurrentSources)d) + ), + new MessengerFactoryEntry( + typeof(ISwitchedOutput), + (d, mp, ck) => new ISwitchedOutputMessenger( + $"{d.Key}-switchedOutput-{ck}", (ISwitchedOutput)d, mp) + ), + + // ── Device info / levels / inputs ───────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IDeviceInfoProvider), + (d, mp, ck) => new IDeviceInfoProviderMessenger( + $"{d.Key}-deviceInfo-{ck}", mp, (IDeviceInfoProvider)d) + ), + new MessengerFactoryEntry( + typeof(ILevelControls), + (d, mp, ck) => new ILevelControlsMessenger( + $"{d.Key}-levelControls-{ck}", mp, (ILevelControls)d) + ), + new MessengerFactoryEntry( + typeof(IHasInputs), + (d, mp, ck) => new IHasInputsMessenger( + $"{d.Key}-inputs-{ck}", mp, (IHasInputs)d) + ), + new MessengerFactoryEntry( + typeof(IHasInputs), + (d, mp, ck) => new IHasInputsMessenger( + $"{d.Key}-inputs-{ck}", mp, (IHasInputs)d) + ), + new MessengerFactoryEntry( + typeof(IHasInputs), + (d, mp, ck) => new IHasInputsMessenger( + $"{d.Key}-inputs-{ck}", mp, (IHasInputs)d) + ), + + // ── Matrix routing ──────────────────────────────────────────────────────── + // Preserving original key format (no controller key suffix) + new MessengerFactoryEntry( + typeof(IMatrixRouting), + (d, mp, _) => new IMatrixRoutingMessenger( + $"{d.Key}-matrixRouting", mp, (IMatrixRouting)d) + ), + + // ── Environmental sensors ───────────────────────────────────────────────── + // Preserving original key format (no controller key suffix) + new MessengerFactoryEntry( + typeof(ITemperatureSensor), + (d, mp, _) => new ITemperatureSensorMessenger( + $"{d.Key}-tempSensor", (ITemperatureSensor)d, mp) + ), + new MessengerFactoryEntry( + typeof(IHumiditySensor), + (d, mp, _) => new IHumiditySensorMessenger( + $"{d.Key}-humiditySensor", (IHumiditySensor)d, mp) + ), + + // ── Room combining ──────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IEssentialsRoomCombiner), + (d, mp, ck) => new IEssentialsRoomCombinerMessenger( + $"{d.Key}-roomCombiner-{ck}", mp, (IEssentialsRoomCombiner)d) + ), + + // ── Projector screen ────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IProjectorScreenLiftControl), + (d, mp, ck) => new IProjectorScreenLiftControlMessenger( + $"{d.Key}-screenLiftControl-{ck}", mp, (IProjectorScreenLiftControl)d) + ), + + // ── DSP ─────────────────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IDspPresets), + (d, mp, ck) => new IDspPresetsMessenger( + $"{d.Key}-dspPresets-{ck}", mp, (IDspPresets)d) + ), + + // Room Entries + // ── Event schedule ──────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IRoomEventSchedule), + (d, mp, ck) => new RoomEventScheduleMessenger( + $"{d.Key}-schedule-{ck}", mp, (IRoomEventSchedule)d) + ), + + // ── Tech password ───────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(ITechPassword), + (d, mp, ck) => new ITechPasswordMessenger( + $"{d.Key}-techPassword-{ck}", mp, (ITechPassword)d) + ), + + // ── Shutdown prompt timer ───────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IShutdownPromptTimer), + (d, mp, ck) => new IShutdownPromptTimerMessenger( + $"{d.Key}-shutdownPromptTimer-{ck}", mp, (IShutdownPromptTimer)d) + ), + + // ── Level controls ──────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(ILevelControls), + (d, mp, ck) => new ILevelControlsMessenger( + $"{d.Key}-levelControls-{ck}", mp, (ILevelControls)d) + ), + }; + } +} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 1b0580f6..5e3314aa 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -17,22 +17,10 @@ using PepperDash.Essentials.AppServer; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; -using PepperDash.Essentials.Core.CrestronIO; -using PepperDash.Essentials.Core.DeviceInfo; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using PepperDash.Essentials.Core.Lighting; using PepperDash.Essentials.Core.Monitoring; using PepperDash.Essentials.Core.Queues; -using PepperDash.Essentials.Core.Routing; -using PepperDash.Essentials.Core.Shades; using PepperDash.Essentials.Core.Web; -using PepperDash.Essentials.Devices.Common.AudioCodec; -using PepperDash.Essentials.Devices.Common.Cameras; -using PepperDash.Essentials.Devices.Common.Displays; -using PepperDash.Essentials.Devices.Common.Lighting; -using PepperDash.Essentials.Devices.Common.SoftCodec; -using PepperDash.Essentials.Devices.Common.VideoCodec; -using PepperDash.Essentials.Room.MobileControl; using PepperDash.Essentials.RoomBridges; using PepperDash.Essentials.Services; using PepperDash.Essentials.WebApiHandlers; @@ -138,7 +126,7 @@ namespace PepperDash.Essentials "No system_url value defined in config. Checking for value from SIMPL Bridge." ); - if (!string.IsNullOrEmpty(SystemUrl)) + if (string.IsNullOrEmpty(SystemUrl)) { this.LogError( "No system_url value defined in config or SIMPL Bridge. Unable to connect to Mobile Control." @@ -254,10 +242,6 @@ namespace PepperDash.Essentials AddPreActivationAction(() => LinkSystemMonitorToAppServer()); - AddPreActivationAction(() => SetupDefaultDeviceMessengers()); - - AddPreActivationAction(() => SetupDefaultRoomMessengers()); - AddPreActivationAction(() => AddWebApiPaths()); AddPreActivationAction(() => @@ -282,765 +266,36 @@ namespace PepperDash.Essentials }); } - private void SetupDefaultRoomMessengers() + /// + public void AddDefaultMessengersForDevice(EssentialsDevice device, IEnumerable interfaces = null) { - this.LogVerbose("Setting up room messengers"); + var interfaceSet = interfaces != null ? new HashSet(interfaces) : [.. device.GetType().GetInterfaces()]; + var messengerAdded = false; - foreach (var room in DeviceManager.AllDevices.OfType()) + foreach (var entry in MessengerFactoryRegistry.Entries) { - this.LogVerbose( - "Setting up room messengers for room: {key}", - room.Key - ); + if (interfaceSet != null && !interfaceSet.Contains(entry.InterfaceType)) + continue; - var messenger = new MobileControlEssentialsRoomBridge(room); + if (!entry.Matches(device)) + continue; - messenger.AddParent(this); + var messenger = entry.Factory(device, $"/device/{device.Key}", Key); - _roomBridges.Add(messenger); + if (messenger == null) + continue; AddDefaultDeviceMessenger(messenger); - - this.LogVerbose( - "Attempting to set up default room messengers for room: {0}", - room.Key - ); - - if (room is IRoomEventSchedule) - { - this.LogInformation("Setting up event schedule messenger for room: {key}", room.Key); - - var scheduleMessenger = new RoomEventScheduleMessenger( - $"{room.Key}-schedule-{Key}", - string.Format("/room/{0}", room.Key), - room as IRoomEventSchedule - ); - - AddDefaultDeviceMessenger(scheduleMessenger); - } - - if (room is ITechPassword) - { - this.LogInformation("Setting up tech password messenger for room: {key}", room.Key); - - var techPasswordMessenger = new ITechPasswordMessenger( - $"{room.Key}-techPassword-{Key}", - string.Format("/room/{0}", room.Key), - room as ITechPassword - ); - - AddDefaultDeviceMessenger(techPasswordMessenger); - } - - if (room is IShutdownPromptTimer) - { - this.LogInformation("Setting up shutdown prompt timer messenger for room: {key}", this, room.Key); - - var shutdownPromptTimerMessenger = new IShutdownPromptTimerMessenger( - $"{room.Key}-shutdownPromptTimer-{Key}", - string.Format("/room/{0}", room.Key), - room as IShutdownPromptTimer - ); - - AddDefaultDeviceMessenger(shutdownPromptTimerMessenger); - } - - if (room is ILevelControls levelControls) - { - this.LogInformation("Setting up level controls messenger for room: {key}", this, room.Key); - - var levelControlsMessenger = new ILevelControlsMessenger( - $"{room.Key}-levelControls-{Key}", - $"/room/{room.Key}", - levelControls - ); - - AddDefaultDeviceMessenger(levelControlsMessenger); - } + messengerAdded = true; } - } - /// - /// Set up the messengers for each device type - /// - private void SetupDefaultDeviceMessengers() - { - bool messengerAdded = false; - - var allDevices = DeviceManager.AllDevices.Where((d) => !(d is IEssentialsRoom)); - - this.LogInformation( - "All Devices that aren't rooms count: {0}", - allDevices?.Count() - ); - - var count = allDevices.Count(); - - foreach (var device in allDevices) + if (!messengerAdded) { - try - { - this.LogVerbose( - "Attempting to set up device messengers for {deviceKey}", - device.Key - ); - - // StatusMonitorBase which is prop of ICommunicationMonitor is not a PepperDash.Core.Device, but is in the device array - if (device is ICommunicationMonitor) - { - this.LogVerbose( - "Checking if {deviceKey} implements ICommunicationMonitor", - device.Key - ); - - if (!(device is ICommunicationMonitor commMonitor)) - { - this.LogDebug( - "{deviceKey} does not implement ICommunicationMonitor. Skipping CommunicationMonitorMessenger", - device.Key - ); - - this.LogDebug("Created all messengers for {deviceKey}. Devices Left: {deviceCount}", device.Key, --count); - - continue; - } - - this.LogDebug( - "Adding CommunicationMonitorMessenger for {deviceKey}", - device.Key - ); - - var commMessenger = new ICommunicationMonitorMessenger( - $"{device.Key}-commMonitor-{Key}", - string.Format("/device/{0}", device.Key), - commMonitor - ); - - AddDefaultDeviceMessenger(commMessenger); - - messengerAdded = true; - } - - // Default to IHasCameraControls if CameraBase and IHasCameraControls - if (device is CameraBase cameraDevice && !(device is IHasCameraControls)) - { - this.LogVerbose( - "Adding CameraBaseMessenger for {deviceKey}", - device.Key - ); - - var cameraMessenger = new CameraBaseMessenger( - $"{device.Key}-cameraBase-{Key}", - cameraDevice, - $"/device/{device.Key}" - ); - - AddDefaultDeviceMessenger(cameraMessenger); - - messengerAdded = true; - } - - if (device is IHasCameraControls cameraControlDev) - { - this.LogVerbose( - "Adding IHasCamerasWithControlMessenger for {deviceKey}", - device.Key - ); - var cameraControlMessenger = new CameraBaseMessenger( - $"{device.Key}-hasCamerasWithControls-{Key}", - cameraControlDev, - $"/device/{device.Key}" - ); - AddDefaultDeviceMessenger(cameraControlMessenger); - messengerAdded = true; - } - - if (device is BlueJeansPc) - { - this.LogVerbose( - "Adding IRunRouteActionMessnger for {deviceKey}", - device.Key - ); - - var routeMessenger = new RunRouteActionMessenger( - $"{device.Key}-runRouteAction-{Key}", - device as BlueJeansPc, - $"/device/{device.Key}" - ); - - AddDefaultDeviceMessenger(routeMessenger); - - messengerAdded = true; - } - - if (device is ITvPresetsProvider) - { - this.LogVerbose( - "Trying to cast to ITvPresetsProvider for {deviceKey}", - device.Key - ); - - var presetsDevice = device as ITvPresetsProvider; - - - this.LogVerbose( - "Adding ITvPresetsProvider for {deviceKey}", - device.Key - ); - - var presetsMessenger = new DevicePresetsModelMessenger( - $"{device.Key}-presets-{Key}", - $"/device/{device.Key}", - presetsDevice - ); - - AddDefaultDeviceMessenger(presetsMessenger); - - messengerAdded = true; - } - - - if (device is DisplayBase) - { - this.LogVerbose("Adding actions for device: {0}", device.Key); - - var dbMessenger = new DisplayBaseMessenger( - $"{device.Key}-displayBase-{Key}", - $"/device/{device.Key}", - device as DisplayBase - ); - - AddDefaultDeviceMessenger(dbMessenger); - - messengerAdded = true; - } - - if (device is TwoWayDisplayBase twoWayDisplay) - { - this.LogVerbose( - "Adding TwoWayDisplayBase for {deviceKey}", - device.Key - ); - var twoWayDisplayMessenger = new TwoWayDisplayBaseMessenger( - $"{device.Key}-twoWayDisplay-{Key}", - string.Format("/device/{0}", device.Key), - twoWayDisplay - ); - AddDefaultDeviceMessenger(twoWayDisplayMessenger); - - messengerAdded = true; - } - - if (device is IBasicVolumeControls) - { - var deviceKey = device.Key; - this.LogVerbose( - "Adding IBasicVolumeControls for {deviceKey}", - deviceKey - ); - - var volControlDevice = device as IBasicVolumeControls; - var messenger = new DeviceVolumeMessenger( - $"{device.Key}-volume-{Key}", - string.Format("/device/{0}", deviceKey), - volControlDevice - ); - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IBasicVideoMuteWithFeedback) - { - var deviceKey = device.Key; - this.LogVerbose( - "Adding IBasicVideoMuteWithFeedback for {deviceKey}", - deviceKey - ); - - var videoMuteControlDevice = device as IBasicVideoMuteWithFeedback; - var messenger = new IBasicVideoMuteWithFeedbackMessenger( - $"{device.Key}-videoMute-{Key}", - string.Format("/device/{0}", deviceKey), - videoMuteControlDevice - ); - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is ILightingScenes || device is LightingBase) - { - var deviceKey = device.Key; - - this.LogVerbose( - "Adding LightingBaseMessenger for {deviceKey}", - deviceKey - ); - - var lightingDevice = device as ILightingScenes; - var messenger = new ILightingScenesMessenger( - $"{device.Key}-lighting-{Key}", - lightingDevice, - string.Format("/device/{0}", deviceKey) - ); - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IShadesOpenCloseStop) - { - var deviceKey = device.Key; - var shadeDevice = device as IShadesOpenCloseStop; - - this.LogVerbose( - "Adding ShadeBaseMessenger for {deviceKey}", - deviceKey - ); - - var messenger = new IShadesOpenCloseStopMessenger( - $"{device.Key}-shades-{Key}", - shadeDevice, - string.Format("/device/{0}", deviceKey) - ); - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is VideoCodecBase codec) - { - this.LogVerbose( - "Adding VideoCodecBaseMessenger for {deviceKey}", codec.Key); - - var messenger = new VideoCodecBaseMessenger( - $"{codec.Key}-videoCodec-{Key}", - codec, - $"/device/{codec.Key}" - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is AudioCodecBase audioCodec) - { - this.LogVerbose( - "Adding AudioCodecBaseMessenger for {deviceKey}", audioCodec.Key - ); - - var messenger = new AudioCodecBaseMessenger( - $"{audioCodec.Key}-audioCodec-{Key}", - audioCodec, - $"/device/{audioCodec.Key}" - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is ISetTopBoxControls stbDevice) - { - this.LogVerbose( - "Adding ISetTopBoxControlMessenger for {deviceKey}" - ); - - var messenger = new ISetTopBoxControlsMessenger( - $"{device.Key}-stb-{Key}", - $"/device/{device.Key}", - stbDevice - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IChannel channelDevice) - { - this.LogVerbose( - "Adding IChannelMessenger for {deviceKey}", device.Key - ); - - var messenger = new IChannelMessenger( - $"{device.Key}-channel-{Key}", - $"/device/{device.Key}", - channelDevice - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IColor colorDevice) - { - this.LogVerbose("Adding IColorMessenger for {deviceKey}", device.Key); - - var messenger = new IColorMessenger( - $"{device.Key}-color-{Key}", - $"/device/{device.Key}", - colorDevice - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IDPad dPadDevice) - { - this.LogVerbose("Adding IDPadMessenger for {deviceKey}", device.Key); - - var messenger = new IDPadMessenger( - $"{device.Key}-dPad-{Key}", - $"/device/{device.Key}", - dPadDevice - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is INumericKeypad nkDevice) - { - this.LogVerbose("Adding INumericKeyapdMessenger for {deviceKey}", device.Key); - - var messenger = new INumericKeypadMessenger( - $"{device.Key}-numericKeypad-{Key}", - $"/device/{device.Key}", - nkDevice - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IHasPowerControl pcDevice) - { - this.LogVerbose("Adding IHasPowerControlMessenger for {deviceKey}", device.Key); - - var messenger = new IHasPowerMessenger( - $"{device.Key}-powerControl-{Key}", - $"/device/{device.Key}", - pcDevice - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IHasPowerControlWithFeedback powerControl) - { - var deviceKey = device.Key; - this.LogVerbose( - "Adding IHasPowerControlWithFeedbackMessenger for {deviceKey}", - deviceKey - ); - - var messenger = new IHasPowerControlWithFeedbackMessenger( - $"{device.Key}-powerFeedback-{Key}", - string.Format("/device/{0}", deviceKey), - powerControl - ); - AddDefaultDeviceMessenger(messenger); - messengerAdded = true; - } - - if (device is ITransport transportDevice) - { - this.LogVerbose( - "Adding ITransportMessenger for {deviceKey}", device.Key - ); - - var messenger = new ITransportMessenger( - $"{device.Key}-transport-{Key}", - $"/device/{device.Key}", - transportDevice - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is ICurrentSources currentSources) - { - this.LogVerbose("Adding CurrentSourcesMessenger for {deviceKey}", device.Key); - - var messenger = new ICurrentSourcesMessenger($"{device.Key}-currentSources-{Key}", $"/device/{device.Key}", currentSources); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is ISwitchedOutput switchedDevice) - { - this.LogVerbose( - "Adding ISwitchedOutputMessenger for {deviceKey}", device.Key - ); - - var messenger = new ISwitchedOutputMessenger( - $"{device.Key}-switchedOutput-{Key}", - switchedDevice, - $"/device/{device.Key}" - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IDeviceInfoProvider provider) - { - this.LogVerbose("Adding IHasDeviceInfoMessenger for {deviceKey}", device.Key - ); - - var messenger = new IDeviceInfoProviderMessenger( - $"{device.Key}-deviceInfo-{Key}", - $"/device/{device.Key}", - provider - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is ILevelControls levelControls) - { - this.LogVerbose( - "Adding LevelControlsMessenger for {deviceKey}", device.Key - ); - - var messenger = new ILevelControlsMessenger( - $"{device.Key}-levelControls-{Key}", - $"/device/{device.Key}", - levelControls - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IHasInputs stringInputs) - { - this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); - - var messenger = new IHasInputsMessenger( - $"{device.Key}-inputs-{Key}", - $"/device/{device.Key}", - stringInputs - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IHasInputs byteInputs) - { - this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); - - var messenger = new IHasInputsMessenger( - $"{device.Key}-inputs-{Key}", - $"/device/{device.Key}", - byteInputs - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IHasInputs intInputs) - { - this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); - - var messenger = new IHasInputsMessenger( - $"{device.Key}-inputs-{Key}", - $"/device/{device.Key}", - intInputs - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IMatrixRouting matrix) - { - this.LogVerbose( - "Adding IMatrixRoutingMessenger for {deviceKey}", - device.Key - ); - - var messenger = new IMatrixRoutingMessenger( - $"{device.Key}-matrixRouting", - $"/device/{device.Key}", - matrix - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is ITemperatureSensor tempSensor) - { - this.LogVerbose( - "Adding ITemperatureSensor for {deviceKey}", - device.Key - ); - - var messenger = new ITemperatureSensorMessenger( - $"{device.Key}-tempSensor", - tempSensor, - $"/device/{device.Key}" - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IHumiditySensor humSensor) - { - this.LogVerbose( - "Adding IHumiditySensor for {deviceKey}", - device.Key - ); - - var messenger = new IHumiditySensorMessenger( - $"{device.Key}-humiditySensor", - humSensor, - $"/device/{device.Key}" - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IEssentialsRoomCombiner roomCombiner) - { - this.LogVerbose( - "Adding IEssentialsRoomCombinerMessenger for {deviceKey}", device.Key - ); - - var messenger = new IEssentialsRoomCombinerMessenger( - $"{device.Key}-roomCombiner-{Key}", - $"/device/{device.Key}", - roomCombiner - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IProjectorScreenLiftControl screenLiftControl) - { - this.LogVerbose("Adding IProjectorScreenLiftControlMessenger for {deviceKey}", device.Key - ); - - var messenger = new IProjectorScreenLiftControlMessenger( - $"{device.Key}-screenLiftControl-{Key}", - $"/device/{device.Key}", - screenLiftControl - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IDspPresets dspPresets) - { - this.LogVerbose("Adding IDspPresetsMessenger for {deviceKey}", device.Key - ); - - var messenger = new IDspPresetsMessenger( - $"{device.Key}-dspPresets-{Key}", - $"/device/{device.Key}", - dspPresets - ); - - AddDefaultDeviceMessenger(messenger); - - messengerAdded = true; - } - - if (device is IHasCamerasWithControls cameras2) - { - this.LogVerbose("Adding IHasCamerasWithControlsMessenger for {deviceKey}", device.Key - ); - var messenger = new IHasCamerasWithControlMessenger( - $"{device.Key}-cameras-{Key}", - $"/device/{device.Key}", - cameras2 - ); - AddDefaultDeviceMessenger(messenger); - messengerAdded = true; - } - - this.LogVerbose("Trying to cast to generic device for device: {key}", device.Key); - - if (device is EssentialsDevice) - { - if (!(device is EssentialsDevice genericDevice) || messengerAdded) - { - this.LogVerbose( - "Skipping GenericMessenger for {deviceKey}. Messenger(s) Added: {messengersAdded}.", - device.Key, - messengerAdded - ); - this.LogDebug( - "AllDevices Completed a device. Devices Left: {count}", - --count - ); - continue; - } - - this.LogDebug( - "Adding GenericMessenger for {deviceKey}", - this, - genericDevice?.Key - ); - - AddDefaultDeviceMessenger( - new GenericMessenger( - genericDevice.Key + "-" + Key + "-generic", - genericDevice, - string.Format("/device/{0}", genericDevice.Key) - ) - ); - } - else - { - this.LogVerbose( - "Not Essentials Device. Skipping GenericMessenger for {deviceKey}", - device.Key - ); - } - this.LogDebug( - "AllDevices Completed a device. Devices Left: {count}", - --count - ); - } - - catch (Exception ex) - { - this.LogException(ex, "Exception setting up default device messengers"); - } + this.LogDebug("Adding GenericMessenger for {deviceKey}", device.Key); + AddDefaultDeviceMessenger(new GenericMessenger( + $"{device.Key}-{Key}-generic", + device, + $"/device/{device.Key}")); } } From dfc1d2bb21fea36e9c0391d63009c173f9e285df Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 6 May 2026 13:54:10 -0600 Subject: [PATCH 056/161] refactor: Rename DisplayBaseMessenger to IRoutingSinkWithSwitchingMessenger and update related references --- src/PepperDash.Core/Logging/Debug.cs | 3 --- .../DeviceTypeExtensions/DisplayBaseMessenger.cs | 9 +++++---- .../MessengerFactoryRegistry.cs | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 4160da21..fcb262b5 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -6,11 +6,8 @@ using System.Reflection; using System.Text.RegularExpressions; using System.Timers; using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronDataStore; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronLogger; -using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting; -using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert; using PdCore = PepperDash.Core.Abstractions; using PepperDash.Core.Logging; using Serilog; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs index 5ce9261e..1426dc25 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs @@ -3,6 +3,7 @@ using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer; using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; using System.Linq; using DisplayBase = PepperDash.Essentials.Devices.Common.Displays.DisplayBase; @@ -11,17 +12,17 @@ namespace PepperDash.Essentials.Room.MobileControl /// /// Represents a DisplayBaseMessenger /// - public class DisplayBaseMessenger : MessengerBase + public class IRoutingSinkWithSwitchingMessenger : MessengerBase { - private readonly DisplayBase display; + private readonly IRoutingSinkWithSwitching display; /// - /// Create an instance of the class. + /// Create an instance of the class. /// /// /// /// - public DisplayBaseMessenger(string key, string messagePath, DisplayBase device) : base(key, messagePath, device) + public IRoutingSinkWithSwitchingMessenger(string key, string messagePath, IRoutingSinkWithSwitching device) : base(key, messagePath, device) { display = device; } diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index e333cd0b..73e3dc22 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -87,7 +87,7 @@ namespace PepperDash.Essentials $"{d.Key}-cameraBase-{ck}", (CameraBase)d, mp), predicate: d => d is CameraBase && !(d is IHasCameraControls) ), - + new MessengerFactoryEntry( typeof(IHasCameraControls), (d, mp, ck) => new CameraBaseMessenger( @@ -116,9 +116,9 @@ namespace PepperDash.Essentials // ── Displays ───────────────────────────────────────────────────────────── new MessengerFactoryEntry( - typeof(DisplayBase), - (d, mp, ck) => new DisplayBaseMessenger( - $"{d.Key}-displayBase-{ck}", mp, (DisplayBase)d) + typeof(IRoutingSinkWithSwitching), + (d, mp, ck) => new IRoutingSinkWithSwitchingMessenger( + $"{d.Key}-displayBase-{ck}", mp, (IRoutingSinkWithSwitching)d) ), new MessengerFactoryEntry( typeof(TwoWayDisplayBase), From f80aa956490d673ac8554f46ea0e633a2ec9c787 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 6 May 2026 17:13:02 -0600 Subject: [PATCH 057/161] Refactor audio codec and display messaging interfaces - Introduced IDialerCallStatus interface to streamline call status handling in audio codecs. - Updated AudioCodecBase to implement IDialerCallStatus, providing a common structure for call status events. - Added IDisplayCurrentInput interface for display devices to expose current input information. - Created corresponding messenger classes for IDialerCallStatus and IDisplayCurrentInput to facilitate communication. - Implemented CameraControlMessenger for camera devices to manage camera controls and presets. - Added IWarmingCoolingMessenger for warming and cooling devices to handle status updates. - Updated MessengerFactoryRegistry to register new messenger classes and ensure proper device messaging. - Cleaned up CameraBase and removed unnecessary comments and whitespace in various files. Co-authored-by: Copilot --- .../AudioCodec/AudioCodecBase.cs | 24 +++++- .../Interfaces/IDialerCallStatus.cs | 20 +++++ .../Cameras/CameraBase.cs | 4 +- .../Interfaces/IDisplayCurrentInput.cs | 14 +++ ... => IRoutingSinkWithSwitchingMessenger.cs} | 0 ...Messenger.cs => CameraControlMessenger.cs} | 4 +- ...enger.cs => IDialerCallStatusMessenger.cs} | 11 +-- ...er.cs => IDisplayCurrentInputMessenger.cs} | 41 +++------ ...ssenger.cs => ILightingScenesMessenger.cs} | 0 ...nger.cs => IRoomEventScheduleMessenger.cs} | 6 +- ...er.cs => IShadesOpenCloseStopMessenger.cs} | 0 ...nger.cs => ITvPresetsProviderMessenger.cs} | 8 +- .../Messengers/IWarmingCoolingMessenger.cs | 86 +++++++++++++++++++ .../MessengerFactoryRegistry.cs | 27 +++--- 14 files changed, 185 insertions(+), 60 deletions(-) create mode 100644 src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDialerCallStatus.cs create mode 100644 src/PepperDash.Essentials.Devices.Common/Displays/Interfaces/IDisplayCurrentInput.cs rename src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/{DisplayBaseMessenger.cs => IRoutingSinkWithSwitchingMessenger.cs} (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{CameraBaseMessenger.cs => CameraControlMessenger.cs} (98%) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{AudioCodecBaseMessenger.cs => IDialerCallStatusMessenger.cs} (90%) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{TwoWayDisplayBaseMessenger.cs => IDisplayCurrentInputMessenger.cs} (57%) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{LightingBaseMessenger.cs => ILightingScenesMessenger.cs} (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{RoomEventScheduleMessenger.cs => IRoomEventScheduleMessenger.cs} (90%) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{ShadeBaseMessenger.cs => IShadesOpenCloseStopMessenger.cs} (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{DevicePresetsModelMessenger.cs => ITvPresetsProviderMessenger.cs} (93%) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IWarmingCoolingMessenger.cs diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/AudioCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/AudioCodecBase.cs index dc0f98a6..5c0f3885 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/AudioCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/AudioCodecBase.cs @@ -6,11 +6,19 @@ using PepperDash.Essentials.Devices.Common.Codec; namespace PepperDash.Essentials.Devices.Common.AudioCodec; -public abstract class AudioCodecBase : EssentialsDevice, IHasDialer, IUsageTracking, IAudioCodecInfo +/// +/// Base class for audio codecs. Provides common properties and methods for audio codecs, +/// as well as a common implementation of IDialerCallStatus to allow the AudioCodecBaseMessenger +/// to get call status information without requiring the full AudioCodecBase class. +/// This is useful for devices that have dialer call status information but do not need to implement +/// the full AudioCodecBase class. +/// +public abstract class AudioCodecBase : EssentialsDevice, IDialerCallStatus, IUsageTracking, IAudioCodecInfo { - + /// public event EventHandler CallStatusChange; + /// public AudioCodecInfo CodecInfo { get; protected set; } #region IUsageTracking Members @@ -41,8 +49,14 @@ public abstract class AudioCodecBase : EssentialsDevice, IHasDialer, IUsageTrack } // In most cases only a single call can be active + /// public List ActiveCalls { get; set; } + /// + /// Constructor + /// + /// + /// public AudioCodecBase(string key, string name) : base(key, name) { @@ -83,16 +97,22 @@ public abstract class AudioCodecBase : EssentialsDevice, IHasDialer, IUsageTrack #region IHasDialer Members + /// public abstract void Dial(string number); + /// public abstract void EndCall(CodecActiveCallItem activeCall); + /// public abstract void EndAllCalls(); + /// public abstract void AcceptCall(CodecActiveCallItem item); + public abstract void RejectCall(CodecActiveCallItem item); + public abstract void SendDtmf(string digit); #endregion diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDialerCallStatus.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDialerCallStatus.cs new file mode 100644 index 00000000..7c32eecf --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDialerCallStatus.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.Devices.Common.AudioCodec; + +/// +/// Defines the contract for a device that has dialer call status information. This is used to provide a common interface for the AudioCodecBaseMessenger to get call status information without requiring the full AudioCodecBase class +/// +public interface IDialerCallStatus : IHasDialer +{ + /// + /// + /// + AudioCodecInfo CodecInfo { get; } + + /// + /// Gets or sets the list of active calls for the device. + /// + List ActiveCalls { get; set; } +} diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraBase.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraBase.cs index a7b880c9..3ee7b7be 100644 --- a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraBase.cs @@ -1,6 +1,4 @@ - - -using System; +using System; using System.Collections.Generic; using System.Linq; using Crestron.SimplSharpPro.DeviceSupport; diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/Interfaces/IDisplayCurrentInput.cs b/src/PepperDash.Essentials.Devices.Common/Displays/Interfaces/IDisplayCurrentInput.cs new file mode 100644 index 00000000..dde208c8 --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/Displays/Interfaces/IDisplayCurrentInput.cs @@ -0,0 +1,14 @@ +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +{ + /// + /// Defines the contract for a display device that has current input information. This is used to provide a common interface for the TwoWayDisplayBaseMessenger to get current input information without requiring the full TwoWayDisplayBase class + /// + public interface IDisplayCurrentInput : IKeyName +{ + /// + /// Gets the Current Input feedback for the display device. + /// + StringFeedback CurrentInputFeedback { get; } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraControlMessenger.cs similarity index 98% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraControlMessenger.cs index 8c9f0451..3c02ee59 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraControlMessenger.cs @@ -11,7 +11,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Messenger for a CameraBase device /// - public class CameraBaseMessenger : MessengerBase where T : IKeyed + public class CameraControlMessenger : MessengerBase where T : IKeyed { /// /// Gets or sets the Camera @@ -24,7 +24,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// /// - public CameraBaseMessenger(string key, T camera, string messagePath) + public CameraControlMessenger(string key, T camera, string messagePath) : base(key, messagePath, camera as IKeyName) { if (camera == null) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs similarity index 90% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs index a1b30341..d9eacecd 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs @@ -1,20 +1,21 @@ using System; using System.Linq; using Newtonsoft.Json.Linq; +using PepperDash.Core; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Codec; namespace PepperDash.Essentials.AppServer.Messengers { /// - /// Provides a messaging bridge for an AudioCodecBase device + /// Provides a messaging bridge for an IDialerCallStatus device /// - public class AudioCodecBaseMessenger : MessengerBase + public class IDialerCallStatusMessenger : MessengerBase { /// /// Device being bridged /// - public AudioCodecBase Codec { get; private set; } + public IDialerCallStatus Codec { get; private set; } /// /// Constuctor @@ -22,8 +23,8 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// /// - public AudioCodecBaseMessenger(string key, AudioCodecBase codec, string messagePath) - : base(key, messagePath, codec) + public IDialerCallStatusMessenger(string key, IDialerCallStatus codec, string messagePath) + : base(key, messagePath, codec as IKeyName) { Codec = codec ?? throw new ArgumentNullException("codec"); codec.CallStatusChange += Codec_CallStatusChange; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDisplayCurrentInputMessenger.cs similarity index 57% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDisplayCurrentInputMessenger.cs index 77a7cc95..0239ecf9 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDisplayCurrentInputMessenger.cs @@ -1,25 +1,27 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using PepperDash.Core; using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Devices.Common.Displays; namespace PepperDash.Essentials.AppServer.Messengers { /// - /// Represents a TwoWayDisplayBaseMessenger + /// Represents a messenger for a display device that has current input information. /// - public class TwoWayDisplayBaseMessenger : MessengerBase + public class IDisplayCurrentInputMessenger : MessengerBase { - private readonly TwoWayDisplayBase _display; + private readonly IDisplayCurrentInput _display; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// /// - public TwoWayDisplayBaseMessenger(string key, string messagePath, TwoWayDisplayBase display) - : base(key, messagePath, display) + public IDisplayCurrentInputMessenger(string key, string messagePath, IDisplayCurrentInput display) + : base(key, messagePath, display as IKeyName) { _display = display; } @@ -31,9 +33,8 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public void SendFullStatus(string id = null) { - var messageObj = new TwoWayDisplayBaseStateMessage + var messageObj = new CurrentInputStateMessage { - //PowerState = _display.PowerIsOnFeedback.BoolValue, CurrentInput = _display.CurrentInputFeedback.StringValue }; @@ -47,11 +48,9 @@ namespace PepperDash.Essentials.AppServer.Messengers AddAction("/fullStatus", (id, content) => SendFullStatus(id)); - AddAction("/displayStatus", (id, content) => SendFullStatus(id)); + AddAction("/currentInputStatus", (id, content) => SendFullStatus(id)); _display.CurrentInputFeedback.OutputChange += CurrentInputFeedbackOnOutputChange; - _display.IsCoolingDownFeedback.OutputChange += IsCoolingFeedbackOnOutputChange; - _display.IsWarmingUpFeedback.OutputChange += IsWarmingFeedbackOnOutputChange; } private void CurrentInputFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) @@ -61,24 +60,6 @@ namespace PepperDash.Essentials.AppServer.Messengers currentInput = feedbackEventArgs.StringValue }) ); - } - - private void IsWarmingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) - { - PostStatusMessage(JToken.FromObject(new - { - isWarming = feedbackEventArgs.BoolValue - }) - ); - } - - private void IsCoolingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) - { - PostStatusMessage(JToken.FromObject(new - { - isCooling = feedbackEventArgs.BoolValue - }) - ); } @@ -89,7 +70,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Represents a TwoWayDisplayBaseStateMessage /// - public class TwoWayDisplayBaseStateMessage : DeviceStateMessageBase + public class CurrentInputStateMessage : DeviceStateMessageBase { //[JsonProperty("powerState", NullValueHandling = NullValueHandling.Ignore)] //public bool? PowerState { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILightingScenesMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILightingScenesMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoomEventScheduleMessenger.cs similarity index 90% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoomEventScheduleMessenger.cs index 8969dcd3..1112a85d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoomEventScheduleMessenger.cs @@ -11,17 +11,17 @@ namespace PepperDash.Essentials.AppServer.Messengers; /// /// Represents a RoomEventScheduleMessenger /// -public class RoomEventScheduleMessenger : MessengerBase +public class IRoomEventScheduleMessenger : MessengerBase { private readonly IRoomEventSchedule _room; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// /// - public RoomEventScheduleMessenger(string key, string messagePath, IRoomEventSchedule room) + public IRoomEventScheduleMessenger(string key, string messagePath, IRoomEventSchedule room) : base(key, messagePath, room as IKeyName) { _room = room; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShadesOpenCloseStopMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShadesOpenCloseStopMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITvPresetsProviderMessenger.cs similarity index 93% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITvPresetsProviderMessenger.cs index 833a781b..fa9bd3b4 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITvPresetsProviderMessenger.cs @@ -10,19 +10,19 @@ using PepperDash.Essentials.Core.Presets; namespace PepperDash.Essentials.AppServer.Messengers { /// - /// Represents a DevicePresetsModelMessenger + /// Represents a ITvPresetsProviderMessenger /// - public class DevicePresetsModelMessenger : MessengerBase + public class ITvPresetsProviderMessenger : MessengerBase { private readonly ITvPresetsProvider _presetsDevice; /// - /// Constructor for DevicePresetsModelMessenger + /// Constructor for ITvPresetsProviderMessenger /// /// The key. /// The message path. /// The presets device. - public DevicePresetsModelMessenger(string key, string messagePath, ITvPresetsProvider presetsDevice) + public ITvPresetsProviderMessenger(string key, string messagePath, ITvPresetsProvider presetsDevice) : base(key, messagePath, presetsDevice as Device) { _presetsDevice = presetsDevice; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IWarmingCoolingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IWarmingCoolingMessenger.cs new file mode 100644 index 00000000..54687e7b --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IWarmingCoolingMessenger.cs @@ -0,0 +1,86 @@ +using System.Text.Json.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.AppServer.Messengers; + +/// +/// Messenger for devices that implement +/// +public class IWarmingCoolingMessenger : MessengerBase +{ + private readonly IWarmingCooling device; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public IWarmingCoolingMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + this.device = device as IWarmingCooling; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/warmingCoolingStatus", (id, content) => SendFullStatus(id)); + + device.IsWarmingUpFeedback.OutputChange += IsWarmingFeedbackOnOutputChange; + device.IsCoolingDownFeedback.OutputChange += IsCoolingFeedbackOnOutputChange; + } + + private void IsWarmingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + isWarming = feedbackEventArgs.BoolValue + })); + } + + private void IsCoolingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + isCooling = feedbackEventArgs.BoolValue + })); + } + + private void SendFullStatus(string id = null) + { + var messageObj = new IWarmingCoolingStateMessage + { + IsWarming = device.IsWarmingUpFeedback.BoolValue, + IsCooling = device.IsCoolingDownFeedback.BoolValue + }; + + PostStatusMessage(messageObj, id); + } +} + + +/// +/// Message object for warming/cooling status +/// +public class IWarmingCoolingStateMessage : DeviceStateMessageBase +{ + /// + /// Indicates whether the device is currently warming up. + /// + [JsonProperty("isWarming", NullValueHandling = NullValueHandling.Ignore)] + public bool IsWarming { get; set; } + + /// + /// Indicates whether the device is currently cooling down. + /// + [JsonProperty("isCooling", NullValueHandling = NullValueHandling.Ignore)] + public bool IsCooling { get; set; } +} + diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index 73e3dc22..060e594f 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -83,14 +83,14 @@ namespace PepperDash.Essentials // CameraBase only when the device does NOT also implement IHasCameraControls new MessengerFactoryEntry( typeof(CameraBase), - (d, mp, ck) => new CameraBaseMessenger( + (d, mp, ck) => new CameraControlMessenger( $"{d.Key}-cameraBase-{ck}", (CameraBase)d, mp), predicate: d => d is CameraBase && !(d is IHasCameraControls) ), new MessengerFactoryEntry( typeof(IHasCameraControls), - (d, mp, ck) => new CameraBaseMessenger( + (d, mp, ck) => new CameraControlMessenger( $"{d.Key}-hasCamerasWithControls-{ck}", (IHasCameraControls)d, mp) ), new MessengerFactoryEntry( @@ -102,7 +102,7 @@ namespace PepperDash.Essentials // ── Routing ────────────────────────────────────────────────────────────── // BlueJeansPc implements IRunRouteAction new MessengerFactoryEntry( - typeof(BlueJeansPc), + typeof(IRunRouteAction), (d, mp, ck) => new RunRouteActionMessenger( $"{d.Key}-runRouteAction-{ck}", (IRunRouteAction)d, mp) ), @@ -110,7 +110,7 @@ namespace PepperDash.Essentials // ── Presets ────────────────────────────────────────────────────────────── new MessengerFactoryEntry( typeof(ITvPresetsProvider), - (d, mp, ck) => new DevicePresetsModelMessenger( + (d, mp, ck) => new ITvPresetsProviderMessenger( $"{d.Key}-presets-{ck}", mp, (ITvPresetsProvider)d) ), @@ -121,9 +121,14 @@ namespace PepperDash.Essentials $"{d.Key}-displayBase-{ck}", mp, (IRoutingSinkWithSwitching)d) ), new MessengerFactoryEntry( - typeof(TwoWayDisplayBase), - (d, mp, ck) => new TwoWayDisplayBaseMessenger( - $"{d.Key}-twoWayDisplay-{ck}", mp, (TwoWayDisplayBase)d) + typeof(IDisplayCurrentInput), + (d, mp, ck) => new IDisplayCurrentInputMessenger( + $"{d.Key}-twoWayDisplay-{ck}", mp, (IDisplayCurrentInput)d) + ), + new MessengerFactoryEntry( + typeof(IWarmingCooling), + (d, mp, ck) => new IWarmingCoolingMessenger( + $"{d.Key}-warmingCooling-{ck}", mp, d) ), // ── Audio / Video ───────────────────────────────────────────────────────── @@ -160,9 +165,9 @@ namespace PepperDash.Essentials $"{d.Key}-videoCodec-{ck}", (VideoCodecBase)d, mp) ), new MessengerFactoryEntry( - typeof(AudioCodecBase), - (d, mp, ck) => new AudioCodecBaseMessenger( - $"{d.Key}-audioCodec-{ck}", (AudioCodecBase)d, mp) + typeof(IDialerCallStatus), + (d, mp, ck) => new IDialerCallStatusMessenger( + $"{d.Key}-audioCodec-{ck}", (IDialerCallStatus)d, mp) ), // ── Set-top box controls ────────────────────────────────────────────────── @@ -294,7 +299,7 @@ namespace PepperDash.Essentials // ── Event schedule ──────────────────────────────────────────────────────── new MessengerFactoryEntry( typeof(IRoomEventSchedule), - (d, mp, ck) => new RoomEventScheduleMessenger( + (d, mp, ck) => new IRoomEventScheduleMessenger( $"{d.Key}-schedule-{ck}", mp, (IRoomEventSchedule)d) ), From ff1393b768cd3211c24faf5372960e5cc311bffd Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 6 May 2026 17:13:48 -0600 Subject: [PATCH 058/161] refactor: Simplify namespace declaration and formatting in IDisplayCurrentInput interface --- .../Displays/Interfaces/IDisplayCurrentInput.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/Interfaces/IDisplayCurrentInput.cs b/src/PepperDash.Essentials.Devices.Common/Displays/Interfaces/IDisplayCurrentInput.cs index dde208c8..f2aa91f4 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/Interfaces/IDisplayCurrentInput.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/Interfaces/IDisplayCurrentInput.cs @@ -1,11 +1,11 @@ using PepperDash.Core; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces -{ - /// - /// Defines the contract for a display device that has current input information. This is used to provide a common interface for the TwoWayDisplayBaseMessenger to get current input information without requiring the full TwoWayDisplayBase class - /// - public interface IDisplayCurrentInput : IKeyName +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Defines the contract for a display device that has current input information. This is used to provide a common interface for the TwoWayDisplayBaseMessenger to get current input information without requiring the full TwoWayDisplayBase class +/// +public interface IDisplayCurrentInput : IKeyName { /// /// Gets the Current Input feedback for the display device. From 9a385eef96e1c201e1e5927d01b7b8cc4605b0fa Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 7 May 2026 11:00:49 -0600 Subject: [PATCH 059/161] refactor: Rename LocalConfigPresent to LocalConfigPresentBanner and enhance debug logging in ControlSystem --- .../Config/Essentials/ConfigReader.cs | 2 +- src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs | 8 ++++++++ src/PepperDash.Essentials/ControlSystem.cs | 9 +-------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index e402341e..f1a79d3c 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -21,7 +21,7 @@ namespace PepperDash.Essentials.Core.Config; /// /// Local Config Present Message /// - public const string LocalConfigPresent = + public const string LocalConfigPresentBanner = @" *************************************************** ************* Using Local config file ************* diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index eaf3bab8..fe1a448c 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -305,7 +305,15 @@ public class EssentialsWebApi : EssentialsDevice : $"https://{currentIp}/cws/debug"; Debug.LogMessage(LogEventLevel.Information, this, "Debug App: {debugPath:l}", debugPath); + Debug.LogInformation(this, "Web API initialized and ready to accept requests"); + Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50)); + + var debugAppUrl = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server +? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws/debug" +: $"https://{currentIp}/cws/debug"; + + Debug.LogMessage(LogEventLevel.Information, this, "Developer Tools Web App available at: {debugAppUrl:l}", debugAppUrl); } } diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index a7ba6dc9..700b30d8 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -316,8 +316,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config..."); if (!ConfigReader.LoadConfig() || ConfigReader.ConfigObject == null) { - Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file. Please ensure a valid config file is present and restart the program."); - // return; + Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file."); } CheckPluginVersionsAgainstConfig(); @@ -480,12 +479,6 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization LoadTieLines(); - /*var mobileControl = GetMobileControlDevice(); - - if (mobileControl == null) return; - - mobileControl.LinkSystemMonitorToAppServer();*/ - } /// From bad08fa6db72de5744474d7ac5e65181ba1698fc Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 7 May 2026 12:08:26 -0600 Subject: [PATCH 060/161] refactor: Extract portal URL display logic into a separate method for improved readability --- src/PepperDash.Essentials/ControlSystem.cs | 34 +++++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 700b30d8..1539fcf0 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -177,14 +177,8 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented)); }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => - CrestronConsole.ConsoleCommandResponse( - "This system can be found at the following URLs:{2}" + - "System URL: {0}{2}" + - "Template URL: {1}{2}", - ConfigReader.ConfigObject.SystemUrl, - ConfigReader.ConfigObject.TemplateUrl, - CrestronEnvironment.NewLine), + CrestronConsole.AddNewConsoleCommand( + PrintPortalInfo, "portalinfo", "Shows portal URLS from configuration", ConsoleAccessLevelEnum.AccessOperator); @@ -216,6 +210,30 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig, IInitialization } } + + private void PrintPortalInfo(string args) + { + if (ConfigReader.ConfigObject == null) + { + CrestronConsole.ConsoleCommandResponse("No configuration loaded. Cannot show portal URLs."); + return; + } + + if (string.IsNullOrEmpty(ConfigReader.ConfigObject.SystemUrl) && string.IsNullOrEmpty(ConfigReader.ConfigObject.TemplateUrl)) + { + CrestronConsole.ConsoleCommandResponse("No portal URLs defined in config."); + return; + } + + CrestronConsole.ConsoleCommandResponse( + "This system can be found at the following URLs:{2}" + + "System URL: {0}{2}" + + "Template URL: {1}{2}", + ConfigReader.ConfigObject?.SystemUrl, + ConfigReader.ConfigObject?.TemplateUrl, + CrestronEnvironment.NewLine); + } + /// /// Determines if the program is running on a processor (appliance) or server (VC-4). /// From e19e69d5c0ad74fb6d6e8ad0155dc8c6cedc9686 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 7 May 2026 14:04:14 -0600 Subject: [PATCH 061/161] refactor: Update solution configuration for Debug and Release builds across multiple platforms to ensure all tests run --- PepperDash.Essentials.4Series.sln | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/PepperDash.Essentials.4Series.sln b/PepperDash.Essentials.4Series.sln index a39555e8..0bc995c0 100644 --- a/PepperDash.Essentials.4Series.sln +++ b/PepperDash.Essentials.4Series.sln @@ -166,23 +166,41 @@ Global {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.ActiveCfg = Release|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.Build.0 = Release|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.Build.0 = Debug|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.Build.0 = Release|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.ActiveCfg = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.Build.0 = Release|Any CPU {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.ActiveCfg = Release|Any CPU + {680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.Build.0 = Release|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.ActiveCfg = Debug|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.Build.0 = Debug|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.Build.0 = Release|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.Build.0 = Release|Any CPU {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU + {F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.Build.0 = Release|Any CPU {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU {841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU From ab4a243ffb5ad623664b04a0ba5f69c62938df3f Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 7 May 2026 20:51:14 -0600 Subject: [PATCH 062/161] feat: Add messengers for codec functionalities and directory management - Implemented IHasCodecCamerasMessenger to handle camera selection and control actions. - Created IHasCodecLayoutsMessenger for managing codec layouts. - Developed IHasCodecSelfViewMessenger to manage self-view functionality. - Added IHasDirectoryMessenger for directory operations including fetching and searching. - Introduced IHasFarEndContentStatusMessenger to report far-end content status. - Implemented IPasswordPromptMessenger for handling password prompts. - Updated VideoCodecBaseMessenger to remove redundant directory and call history handling. - Registered new messenger types in MessengerFactoryRegistry. - Added necessary using directives in ControlSystem.cs for UC functionalities. Co-authored-by: Copilot --- .../Config/Essentials/ConfigReader.cs | 5 +- .../Messengers/IHasCallHistoryMessenger.cs | 84 +++ ...cs => IHasCamerasWithControlsMessenger.cs} | 4 +- .../Messengers/IHasCodecCamerasMessenger.cs | 310 +++++++++ .../Messengers/IHasCodecLayoutsMessenger.cs | 64 ++ .../Messengers/IHasCodecSelfViewMessenger.cs | 62 ++ .../Messengers/IHasDirectoryMessenger.cs | 129 ++++ .../IHasFarEndContentStatusMessenger.cs | 55 ++ .../Messengers/IPasswordPromptMessenger.cs | 85 +++ .../Messengers/VideoCodecBaseMessenger.cs | 650 +----------------- .../MessengerFactoryRegistry.cs | 39 +- src/PepperDash.Essentials/ControlSystem.cs | 1 + 12 files changed, 845 insertions(+), 643 deletions(-) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{IHasCamerasWithControlMessenger.cs => IHasCamerasWithControlsMessenger.cs} (95%) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecLayoutsMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasFarEndContentStatusMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IPasswordPromptMessenger.cs diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index f1a79d3c..a1afba93 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -1,11 +1,8 @@ - - -using System; +using System; using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Core.Config; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs new file mode 100644 index 00000000..10e2f8d6 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices implementing + /// + public class IHasCallHistoryMessenger : MessengerBase + { + private readonly IHasCallHistory _callHistory; + + /// + /// Initializes a new instance of the class. + /// + public IHasCallHistoryMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _callHistory = device as IHasCallHistory ?? throw new ArgumentException("device must implement IHasCallHistory", nameof(device)); + _callHistory.CallHistory.RecentCallsListHasChanged += CallHistory_RecentCallsListHasChanged; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => PostCallHistory()); + + AddAction("/getCallHistory", (id, content) => PostCallHistory()); + } + + private void CallHistory_RecentCallsListHasChanged(object sender, EventArgs e) + { + try + { + if (!(sender is CodecCallHistory codecCallHistory)) return; + + var recents = codecCallHistory.RecentCalls; + if (recents != null) + { + PostStatusMessage(new IHasCallHistoryStateMessage + { + RecentCalls = recents + }); + } + } + catch (Exception ex) + { + this.LogError(ex, "Error posting call history"); + } + } + + private void PostCallHistory() + { + try + { + var recents = _callHistory.CallHistory.RecentCalls; + if (recents != null) + { + PostStatusMessage(new IHasCallHistoryStateMessage + { + RecentCalls = recents + }); + } + } + catch (Exception ex) + { + this.LogError(ex, "Error posting call history"); + } + } + } + + public class IHasCallHistoryStateMessage : DeviceStateMessageBase + { + [JsonProperty("recentCalls", NullValueHandling = NullValueHandling.Ignore)] + public List RecentCalls { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlsMessenger.cs similarity index 95% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlsMessenger.cs index cce07dcd..e20eabdc 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlsMessenger.cs @@ -12,7 +12,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Messenger for devices that implement the IHasCameras interface. /// - public class IHasCamerasWithControlMessenger : MessengerBase + public class IHasCamerasWithControlsMessenger : MessengerBase { /// /// Device being bridged that implements IHasCameras interface. @@ -26,7 +26,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// /// - public IHasCamerasWithControlMessenger(string key, string messagePath, IHasCamerasWithControls cameraController) + public IHasCamerasWithControlsMessenger(string key, string messagePath, IHasCamerasWithControls cameraController) : base(key, messagePath, cameraController) { CameraController = cameraController ?? throw new ArgumentNullException("cameraController"); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs new file mode 100644 index 00000000..c3f8d3fd --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices implementing , including + /// sub-interface support for , + /// , and . + /// + public class IHasCodecCamerasMessenger : MessengerBase + { + private readonly VideoCodecBase _codec; + private readonly IHasCodecCameras _cameraCodec; + + /// + /// Initializes a new instance of the class. + /// + public IHasCodecCamerasMessenger(string key, string messagePath, VideoCodecBase codec) + : base(key, messagePath, codec) + { + _codec = codec ?? throw new ArgumentNullException(nameof(codec)); + _cameraCodec = codec as IHasCodecCameras ?? throw new ArgumentException("codec must implement IHasCodecCameras", nameof(codec)); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + this.LogVerbose("Adding IHasCodecCameras Actions"); + + _cameraCodec.CameraSelected += CameraCodec_CameraSelected; + + AddAction("/fullStatus", (id, content) => PostSelectedCamera()); + + AddAction("/cameraSelect", (id, content) => + { + var msg = content.ToObject>(); + _cameraCodec.SelectCamera(msg.Value); + }); + + MapCameraActions(); + + if (_codec is IHasCodecRoomPresets presetsCodec) + { + this.LogVerbose("Adding IHasCodecRoomPresets Actions"); + + presetsCodec.CodecRoomPresetsListHasChanged += PresetsCodec_CameraPresetsListHasChanged; + + AddAction("/cameraPreset", (id, content) => + { + var msg = content.ToObject>(); + presetsCodec.CodecRoomPresetSelect(msg.Value); + }); + + AddAction("/cameraPresetStore", (id, content) => + { + var msg = content.ToObject(); + presetsCodec.CodecRoomPresetStore(msg.ID, msg.Description); + }); + } + + if (_codec is IHasCameraAutoMode speakerTrackCodec) + { + this.LogVerbose("Adding IHasCameraAutoMode Actions"); + + speakerTrackCodec.CameraAutoModeIsOnFeedback.OutputChange += CameraAutoModeIsOnFeedback_OutputChange; + + AddAction("/cameraModeAuto", (id, content) => speakerTrackCodec.CameraAutoModeOn()); + AddAction("/cameraModeManual", (id, content) => speakerTrackCodec.CameraAutoModeOff()); + } + + if (_codec is IHasCameraOff cameraOffCodec) + { + this.LogVerbose("Adding IHasCameraOff Actions"); + + cameraOffCodec.CameraIsOffFeedback.OutputChange += CameraIsOffFeedback_OutputChange; + + AddAction("/cameraModeOff", (id, content) => cameraOffCodec.CameraOff()); + } + } + + private void CameraCodec_CameraSelected(object sender, CameraSelectedEventArgs e) + { + try + { + MapCameraActions(); + PostSelectedCamera(); + } + catch (Exception ex) + { + this.LogError(ex, "Exception handling camera selected event"); + } + } + + private void MapCameraActions() + { + if (_cameraCodec.SelectedCamera == null) return; + + RemoveAction("/cameraUp"); + RemoveAction("/cameraDown"); + RemoveAction("/cameraLeft"); + RemoveAction("/cameraRight"); + RemoveAction("/cameraZoomIn"); + RemoveAction("/cameraZoomOut"); + RemoveAction("/cameraHome"); + + if (_cameraCodec.SelectedCamera is IHasCameraPtzControl camera) + { + AddAction("/cameraUp", (id, content) => HandleCameraPressAndHold(content, b => + { + if (b) camera.TiltUp(); else camera.TiltStop(); + })); + + AddAction("/cameraDown", (id, content) => HandleCameraPressAndHold(content, b => + { + if (b) camera.TiltDown(); else camera.TiltStop(); + })); + + AddAction("/cameraLeft", (id, content) => HandleCameraPressAndHold(content, b => + { + if (b) camera.PanLeft(); else camera.PanStop(); + })); + + AddAction("/cameraRight", (id, content) => HandleCameraPressAndHold(content, b => + { + if (b) camera.PanRight(); else camera.PanStop(); + })); + + AddAction("/cameraZoomIn", (id, content) => HandleCameraPressAndHold(content, b => + { + if (b) camera.ZoomIn(); else camera.ZoomStop(); + })); + + AddAction("/cameraZoomOut", (id, content) => HandleCameraPressAndHold(content, b => + { + if (b) camera.ZoomOut(); else camera.ZoomStop(); + })); + + AddAction("/cameraHome", (id, content) => camera.PositionHome()); + + RemoveAction("/cameraAutoFocus"); + RemoveAction("/cameraFocusNear"); + RemoveAction("/cameraFocusFar"); + + if (_cameraCodec.SelectedCamera is IHasCameraFocusControl focusCamera) + { + AddAction("/cameraAutoFocus", (id, content) => focusCamera.TriggerAutoFocus()); + + AddAction("/cameraFocusNear", (id, content) => HandleCameraPressAndHold(content, b => + { + if (b) focusCamera.FocusNear(); else focusCamera.FocusStop(); + })); + + AddAction("/cameraFocusFar", (id, content) => HandleCameraPressAndHold(content, b => + { + if (b) focusCamera.FocusFar(); else focusCamera.FocusStop(); + })); + } + } + } + + private void HandleCameraPressAndHold(JToken content, Action cameraAction) + { + var state = content.ToObject>(); + var timerHandler = PressAndHoldHandler.GetPressAndHoldHandler(state.Value); + if (timerHandler == null) return; + timerHandler(state.Value, cameraAction); + cameraAction(state.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase)); + } + + private void CameraAutoModeIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + PostCameraMode(); + } + + private void CameraIsOffFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + PostCameraMode(); + } + + private void PresetsCodec_CameraPresetsListHasChanged(object sender, EventArgs e) + { + PostCameraPresets(); + } + + private string GetCameraMode() + { + string m = ""; + + if (_codec is IHasCameraAutoMode speakerTrackCodec) + { + m = speakerTrackCodec.CameraAutoModeIsOnFeedback.BoolValue + ? eCameraControlMode.Auto.ToString().ToLower() + : eCameraControlMode.Manual.ToString().ToLower(); + } + + if (_codec is IHasCameraOff cameraOffCodec && cameraOffCodec.CameraIsOffFeedback.BoolValue) + { + m = eCameraControlMode.Off.ToString().ToLower(); + } + + return m; + } + + private Camera GetSelectedCamera() + { + var camera = new Camera(); + + if (_cameraCodec.SelectedCameraFeedback != null) + camera.Key = _cameraCodec.SelectedCameraFeedback.StringValue; + + if (_cameraCodec.SelectedCamera != null) + { + camera.Name = _cameraCodec.SelectedCamera.Name; + + if (_cameraCodec.SelectedCamera is IHasCameraPtzControl ptz) + { + camera.Capabilities = new CameraCapabilities + { + CanPan = ptz is IHasCameraPanControl, + CanTilt = ptz is IHasCameraTiltControl, + CanZoom = ptz is IHasCameraZoomControl, + CanFocus = ptz is IHasCameraFocusControl, + }; + } + } + + if (_cameraCodec.ControllingFarEndCameraFeedback != null) + camera.IsFarEnd = _cameraCodec.ControllingFarEndCameraFeedback.BoolValue; + + return camera; + } + + private List GetCurrentPresets() + { + if (!(_codec is IHasCodecRoomPresets presetsCodec)) return null; + + if (_codec is IHasFarEndCameraControl farEnd && farEnd.ControllingFarEndCameraFeedback.BoolValue) + return presetsCodec.FarEndRoomPresets; + + return presetsCodec.NearEndPresets; + } + + private void PostCameraMode() + { + try + { + PostStatusMessage(new IHasCodecCamerasStateMessage + { + CameraMode = GetCameraMode() + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting camera mode"); + } + } + + private void PostSelectedCamera() + { + try + { + PostStatusMessage(new IHasCodecCamerasStateMessage + { + Cameras = new CameraStatus { SelectedCamera = GetSelectedCamera() }, + Presets = GetCurrentPresets() + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting selected camera"); + } + } + + private void PostCameraPresets() + { + try + { + PostStatusMessage(new IHasCodecCamerasStateMessage + { + Presets = GetCurrentPresets() + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting camera presets"); + } + } + } + + public class IHasCodecCamerasStateMessage : DeviceStateMessageBase + { + [JsonProperty("cameraMode", NullValueHandling = NullValueHandling.Ignore)] + public string CameraMode { get; set; } + + [JsonProperty("cameras", NullValueHandling = NullValueHandling.Ignore)] + public CameraStatus Cameras { get; set; } + + [JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)] + public List Presets { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecLayoutsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecLayoutsMessenger.cs new file mode 100644 index 00000000..db9cea4d --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecLayoutsMessenger.cs @@ -0,0 +1,64 @@ +using System; +using Newtonsoft.Json; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices implementing + /// + public class IHasCodecLayoutsMessenger : MessengerBase + { + private readonly IHasCodecLayouts _layouts; + + /// + /// Initializes a new instance of the class. + /// + public IHasCodecLayoutsMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _layouts = device as IHasCodecLayouts ?? throw new ArgumentException("device must implement IHasCodecLayouts", nameof(device)); + + _layouts.LocalLayoutFeedback.OutputChange += LocalLayoutFeedback_OutputChange; + } + + private void LocalLayoutFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + SendFullStatus(); + } + + private void SendFullStatus() + { + PostStatusMessage(new IHasCodecLayoutsStateMessage + { + CurrentLayout = _layouts.LocalLayoutFeedback.StringValue + }); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + AddAction("/layoutStatus", (id, content) => SendFullStatus()); + + AddAction("/cameraRemoteView", (id, content) => _layouts.LocalLayoutToggle()); + AddAction("/cameraLayout", (id, content) => _layouts.LocalLayoutToggle()); + } + } + + /// + /// State message for + /// + public class IHasCodecLayoutsStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets the current layout of the codec + /// + [JsonProperty("currentLayout", NullValueHandling = NullValueHandling.Ignore)] + public string CurrentLayout { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs new file mode 100644 index 00000000..885c786d --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs @@ -0,0 +1,62 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices implementing + /// + public class IHasCodecSelfViewMessenger : MessengerBase + { + private readonly IHasCodecSelfView _selfView; + + /// + /// Initializes a new instance of the class. + /// + public IHasCodecSelfViewMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _selfView = device as IHasCodecSelfView ?? throw new ArgumentException("device must implement IHasCodecSelfView", nameof(device)); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/cameraSelfView", (id, content) => _selfView.SelfViewModeToggle()); + + _selfView.SelfviewIsOnFeedback.OutputChange += SelfviewIsOnFeedback_OutputChange; + } + + private void SelfviewIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + PostCameraSelfView(); + } + + private void PostCameraSelfView() + { + try + { + PostStatusMessage(new IHasCodecSelfViewStateMessage + { + CameraSelfViewIsOn = _selfView.SelfviewIsOnFeedback.BoolValue + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting camera self view"); + } + } + } + + public class IHasCodecSelfViewStateMessage : DeviceStateMessageBase + { + [JsonProperty("cameraSelfView", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraSelfViewIsOn { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs new file mode 100644 index 00000000..677d31c1 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs @@ -0,0 +1,129 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices implementing + /// + public class IHasDirectoryMessenger : MessengerBase + { + private readonly IHasDirectory _directory; + + /// + /// Initializes a new instance of the class. + /// + public IHasDirectoryMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _directory = device as IHasDirectory ?? throw new ArgumentException("device must implement IHasDirectory", nameof(device)); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/getDirectory", (id, content) => GetDirectoryRoot()); + + AddAction("/directoryById", (id, content) => + { + var msg = content.ToObject>(); + GetDirectory(msg.Value); + }); + + AddAction("/directorySearch", (id, content) => + { + var msg = content.ToObject>(); + GetDirectory(msg.Value); + }); + + AddAction("/directoryBack", (id, content) => GetPreviousDirectory()); + + _directory.DirectoryResultReturned += DirectoryResultReturned; + _directory.PhonebookSyncState.InitialSyncCompleted += PhonebookSyncState_InitialSyncCompleted; + } + + private void DirectoryResultReturned(object sender, DirectoryEventArgs e) + { + SendDirectory(e.Directory); + } + + private void SendDirectory(CodecDirectory directory) + { + try + { + this.LogVerbose("Sending Directory. Directory Item Count: {directoryItemCount}", directory.CurrentDirectoryResults.Count); + Task.Run(() => PostStatusMessage(new IHasDirectoryStateMessage + { + CurrentDirectory = directory + })); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending directory"); + } + } + + private void PhonebookSyncState_InitialSyncCompleted(object sender, EventArgs e) + { + try + { + PostStatusMessage(new IHasDirectoryStateMessage + { + InitialPhonebookSyncComplete = true + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting phonebook sync state"); + } + } + + private void GetDirectory(string id) + { + _directory.GetDirectoryFolderContents(id); + } + + private void GetDirectoryRoot() + { + try + { + if (!_directory.PhonebookSyncState.InitialSyncComplete) + { + PostStatusMessage(new IHasDirectoryStateMessage + { + InitialPhonebookSyncComplete = false + }); + return; + } + + _directory.SetCurrentDirectoryToRoot(); + } + catch (Exception ex) + { + this.LogError(ex, "Error getting directory root"); + } + } + + private void GetPreviousDirectory() + { + _directory.GetDirectoryParentFolderContents(); + } + } + + public class IHasDirectoryStateMessage : DeviceStateMessageBase + { + [JsonProperty("currentDirectory", NullValueHandling = NullValueHandling.Ignore)] + public CodecDirectory CurrentDirectory { get; set; } + + [JsonProperty("initialPhonebookSyncComplete", NullValueHandling = NullValueHandling.Ignore)] + public bool? InitialPhonebookSyncComplete { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasFarEndContentStatusMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasFarEndContentStatusMessenger.cs new file mode 100644 index 00000000..0e307a6f --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasFarEndContentStatusMessenger.cs @@ -0,0 +1,55 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices implementing + /// + public class IHasFarEndContentStatusMessenger : MessengerBase + { + private readonly IHasFarEndContentStatus _device; + + /// + /// Initializes a new instance of the class. + /// + public IHasFarEndContentStatusMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _device = device as IHasFarEndContentStatus ?? throw new ArgumentException("device must implement IHasFarEndContentStatus", nameof(device)); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + _device.ReceivingContent.OutputChange += (sender, args) => PostReceivingContent(args.BoolValue); + } + + private void PostReceivingContent(bool receivingContent) + { + try + { + PostStatusMessage(new IHasFarEndContentStatusStateMessage + { + ReceivingContent = receivingContent + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting receiving content"); + } + } + } + + public class IHasFarEndContentStatusStateMessage : DeviceStateMessageBase + { + [JsonProperty("receivingContent", NullValueHandling = NullValueHandling.Ignore)] + public bool? ReceivingContent { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IPasswordPromptMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IPasswordPromptMessenger.cs new file mode 100644 index 00000000..84ca2fe3 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IPasswordPromptMessenger.cs @@ -0,0 +1,85 @@ +using System; +using Newtonsoft.Json; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices implementing + /// + public class IPasswordPromptMessenger : MessengerBase + { + private readonly IPasswordPrompt _device; + + /// + /// Initializes a new instance of the class. + /// + public IPasswordPromptMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _device = device as IPasswordPrompt ?? throw new ArgumentException("device must implement IPasswordPrompt", nameof(device)); + _device.PasswordRequired += OnPasswordRequired; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/password", (id, content) => + { + var msg = content.ToObject>(); + _device.SubmitPassword(msg.Value); + }); + } + + private void OnPasswordRequired(object sender, PasswordPromptEventArgs args) + { + PostEventMessage(new PasswordPromptEventMessage + { + Message = args.Message, + LastAttemptWasIncorrect = args.LastAttemptWasIncorrect, + LoginAttemptFailed = args.LoginAttemptFailed, + LoginAttemptCancelled = args.LoginAttemptCancelled, + EventType = "passwordPrompt" + }); + } + } + + /// + /// Base event message for video codec events + /// + public class VideoCodecBaseEventMessage : DeviceEventMessageBase + { + } + + /// + /// Event message sent when a password is required + /// + public class PasswordPromptEventMessage : VideoCodecBaseEventMessage + { + /// + /// Gets or sets the Message + /// + [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] + public string Message { get; set; } + + /// + /// Gets or sets the LastAttemptWasIncorrect + /// + [JsonProperty("lastAttemptWasIncorrect", NullValueHandling = NullValueHandling.Ignore)] + public bool LastAttemptWasIncorrect { get; set; } + + /// + /// Gets or sets the LoginAttemptFailed + /// + [JsonProperty("loginAttemptFailed", NullValueHandling = NullValueHandling.Ignore)] + public bool LoginAttemptFailed { get; set; } + + /// + /// Gets or sets the LoginAttemptCancelled + /// + [JsonProperty("loginAttemptCancelled", NullValueHandling = NullValueHandling.Ignore)] + public bool LoginAttemptCancelled { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs index 28f1999f..285d3f3f 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs @@ -38,99 +38,6 @@ namespace PepperDash.Essentials.AppServer.Messengers Codec = codec ?? throw new ArgumentNullException("codec"); codec.CallStatusChange += Codec_CallStatusChange; codec.IsReadyChange += Codec_IsReadyChange; - - if (codec is IHasDirectory dirCodec) - { - dirCodec.DirectoryResultReturned += DirCodec_DirectoryResultReturned; - } - - if (codec is IHasCallHistory recCodec) - { - recCodec.CallHistory.RecentCallsListHasChanged += CallHistory_RecentCallsListHasChanged; - } - - if (codec is IPasswordPrompt pwPromptCodec) - { - pwPromptCodec.PasswordRequired += OnPasswordRequired; - } - } - - private void OnPasswordRequired(object sender, PasswordPromptEventArgs args) - { - var eventMsg = new PasswordPromptEventMessage - { - Message = args.Message, - LastAttemptWasIncorrect = args.LastAttemptWasIncorrect, - LoginAttemptFailed = args.LoginAttemptFailed, - LoginAttemptCancelled = args.LoginAttemptCancelled, - EventType = "passwordPrompt" - }; - - PostEventMessage(eventMsg); - } - - /// - /// - /// - /// - /// - private void CallHistory_RecentCallsListHasChanged(object sender, EventArgs e) - { - try - { - var state = new VideoCodecBaseStateMessage(); - - if (!(sender is CodecCallHistory codecCallHistory)) return; - var recents = codecCallHistory.RecentCalls; - - if (recents != null) - { - state.RecentCalls = recents; - - PostStatusMessage(state); - } - } - catch (Exception ex) - { - this.LogError(ex, "Error posting call history"); - } - } - - /// - /// - /// - /// - /// - protected virtual void DirCodec_DirectoryResultReturned(object sender, DirectoryEventArgs e) - { - if (Codec is IHasDirectory) - SendDirectory(e.Directory); - } - - /// - /// Posts the current directory - /// - protected void SendDirectory(CodecDirectory directory) - { - try - { - var state = new VideoCodecBaseStateMessage(); - - - if (Codec is IHasDirectory dirCodec) - { - this.LogVerbose("Sending Directory. Directory Item Count: {directoryItemCount}", directory.CurrentDirectoryResults.Count); - - //state.CurrentDirectory = PrefixDirectoryFolderItems(directory); - state.CurrentDirectory = directory; - - Task.Run(() => PostStatusMessage(state)); - } - } - catch (Exception ex) - { - this.LogError(ex, "Error sending directory"); - } } /// @@ -217,132 +124,6 @@ namespace PepperDash.Essentials.AppServer.Messengers Codec.SharingContentIsOnFeedback.OutputChange += SharingContentIsOnFeedback_OutputChange; Codec.SharingSourceFeedback.OutputChange += SharingSourceFeedback_OutputChange; - // Directory actions - if (Codec is IHasDirectory dirCodec) - { - AddAction("/getDirectory", (id, content) => GetDirectoryRoot()); - - AddAction("/directoryById", (id, content) => - { - var msg = content.ToObject>(); - GetDirectory(msg.Value); - }); - - AddAction("/directorySearch", (id, content) => - { - var msg = content.ToObject>(); - - GetDirectory(msg.Value); - }); - - AddAction("/directoryBack", (id, content) => GetPreviousDirectory()); - - dirCodec.PhonebookSyncState.InitialSyncCompleted += PhonebookSyncState_InitialSyncCompleted; - } - - // History actions - if (Codec is IHasCallHistory recCodec) - { - AddAction("/getCallHistory", (id, content) => PostCallHistory()); - } - if (Codec is IHasCodecCameras cameraCodec) - { - this.LogVerbose("Adding IHasCodecCameras Actions"); - - cameraCodec.CameraSelected += CameraCodec_CameraSelected; - - AddAction("/cameraSelect", (id, content) => - { - var msg = content.ToObject>(); - - cameraCodec.SelectCamera(msg.Value); - }); - - - MapCameraActions(); - - if (Codec is IHasCodecRoomPresets presetsCodec) - { - this.LogVerbose("Adding IHasCodecRoomPresets Actions"); - - presetsCodec.CodecRoomPresetsListHasChanged += PresetsCodec_CameraPresetsListHasChanged; - - AddAction("/cameraPreset", (id, content) => - { - var msg = content.ToObject>(); - - presetsCodec.CodecRoomPresetSelect(msg.Value); - }); - - AddAction("/cameraPresetStore", (id, content) => - { - var msg = content.ToObject(); - - presetsCodec.CodecRoomPresetStore(msg.ID, msg.Description); - }); - } - - if (Codec is IHasCameraAutoMode speakerTrackCodec) - { - this.LogVerbose("Adding IHasCameraAutoMode Actions"); - - speakerTrackCodec.CameraAutoModeIsOnFeedback.OutputChange += CameraAutoModeIsOnFeedback_OutputChange; - - AddAction("/cameraModeAuto", (id, content) => speakerTrackCodec.CameraAutoModeOn()); - - AddAction("/cameraModeManual", (id, content) => speakerTrackCodec.CameraAutoModeOff()); - } - - if (Codec is IHasCameraOff cameraOffCodec) - { - this.LogVerbose("Adding IHasCameraOff Actions"); - - cameraOffCodec.CameraIsOffFeedback.OutputChange += (CameraIsOffFeedback_OutputChange); - - AddAction("/cameraModeOff", (id, content) => cameraOffCodec.CameraOff()); - } - } - - - - if (Codec is IHasCodecSelfView selfViewCodec) - { - this.LogVerbose("Adding IHasCodecSelfView Actions"); - - AddAction("/cameraSelfView", (id, content) => selfViewCodec.SelfViewModeToggle()); - - selfViewCodec.SelfviewIsOnFeedback.OutputChange += new EventHandler(SelfviewIsOnFeedback_OutputChange); - } - - - if (Codec is IHasCodecLayouts layoutsCodec) - { - this.LogVerbose("Adding IHasCodecLayouts Actions"); - - AddAction("/cameraRemoteView", (id, content) => layoutsCodec.LocalLayoutToggle()); - - AddAction("/cameraLayout", (id, content) => layoutsCodec.LocalLayoutToggle()); - } - - if (Codec is IPasswordPrompt pwCodec) - { - this.LogVerbose("Adding IPasswordPrompt Actions"); - - AddAction("/password", (id, content) => - { - var msg = content.ToObject>(); - - pwCodec.SubmitPassword(msg.Value); - }); - } - - - if (Codec is IHasFarEndContentStatus farEndContentStatus) - { - farEndContentStatus.ReceivingContent.OutputChange += - (sender, args) => PostReceivingContent(args.BoolValue); - } - this.LogVerbose("Adding Privacy & Standby Actions"); AddAction("/privacyModeOn", (id, content) => Codec.PrivacyModeOn()); @@ -393,304 +174,6 @@ namespace PepperDash.Essentials.AppServer.Messengers } } - private void PhonebookSyncState_InitialSyncCompleted(object sender, EventArgs e) - { - try - { - var state = new VideoCodecBaseStateMessage - { - InitialPhonebookSyncComplete = true - }; - - PostStatusMessage(state); - } - catch (Exception ex) - { - this.LogError(ex, "Error posting phonebook sync state"); - } - } - - private void CameraIsOffFeedback_OutputChange(object sender, FeedbackEventArgs e) - { - PostCameraMode(); - } - - private void SelfviewIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) - { - PostCameraSelfView(); - } - - private void PresetsCodec_CameraPresetsListHasChanged(object sender, EventArgs e) - { - PostCameraPresets(); - } - - private void CameraAutoModeIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) - { - PostCameraMode(); - } - - - private void CameraCodec_CameraSelected(object sender, CameraSelectedEventArgs e) - { - try - { - MapCameraActions(); - PostSelectedCamera(); - } - catch (Exception ex) - { - this.LogError(ex, "Exception handling camera selected event"); - } - } - - /// - /// Maps the camera control actions to the current selected camera on the codec - /// - private void MapCameraActions() - { - if (Codec is IHasCamerasWithControls cameraCodec && cameraCodec.SelectedCamera != null) - { - RemoveAction("/cameraUp"); - RemoveAction("/cameraDown"); - RemoveAction("/cameraLeft"); - RemoveAction("/cameraRight"); - RemoveAction("/cameraZoomIn"); - RemoveAction("/cameraZoomOut"); - RemoveAction("/cameraHome"); - - if (cameraCodec.SelectedCamera is IHasCameraPtzControl camera) - { - AddAction("/cameraUp", (id, content) => HandleCameraPressAndHold(content, (b) => - { - if (b) - { - camera.TiltUp(); - return; - } - - camera.TiltStop(); - })); - - AddAction("/cameraDown", (id, content) => HandleCameraPressAndHold(content, (b) => - { - if (b) - { - camera.TiltDown(); - return; - } - - camera.TiltStop(); - })); - - AddAction("/cameraLeft", (id, content) => HandleCameraPressAndHold(content, (b) => - { - if (b) - { - camera.PanLeft(); - return; - } - - camera.PanStop(); - })); - - AddAction("/cameraRight", (id, content) => HandleCameraPressAndHold(content, (b) => - { - if (b) - { - camera.PanRight(); - return; - } - - camera.PanStop(); - })); - - AddAction("/cameraZoomIn", (id, content) => HandleCameraPressAndHold(content, (b) => - { - if (b) - { - camera.ZoomIn(); - return; - } - - camera.ZoomStop(); - })); - - AddAction("/cameraZoomOut", (id, content) => HandleCameraPressAndHold(content, (b) => - { - if (b) - { - camera.ZoomOut(); - return; - } - - camera.ZoomStop(); - })); - AddAction("/cameraHome", (id, content) => camera.PositionHome()); - - - RemoveAction("/cameraAutoFocus"); - RemoveAction("/cameraFocusNear"); - RemoveAction("/cameraFocusFar"); - - if (cameraCodec is IHasCameraFocusControl focusCamera) - { - AddAction("/cameraAutoFocus", (id, content) => focusCamera.TriggerAutoFocus()); - - AddAction("/cameraFocusNear", (id, content) => HandleCameraPressAndHold(content, (b) => - { - if (b) - { - focusCamera.FocusNear(); - return; - } - - focusCamera.FocusStop(); - })); - - AddAction("/cameraFocusFar", (id, content) => HandleCameraPressAndHold(content, (b) => - { - if (b) - { - focusCamera.FocusFar(); - return; - } - - focusCamera.FocusStop(); - })); - } - } - } - } - - private void HandleCameraPressAndHold(JToken content, Action cameraAction) - { - var state = content.ToObject>(); - - var timerHandler = PressAndHoldHandler.GetPressAndHoldHandler(state.Value); - if (timerHandler == null) - { - return; - } - - timerHandler(state.Value, cameraAction); - - cameraAction(state.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase)); - } - - private string GetCameraMode() - { - string m = ""; - - if (Codec is IHasCameraAutoMode speakerTrackCodec) - { - m = speakerTrackCodec.CameraAutoModeIsOnFeedback.BoolValue - ? eCameraControlMode.Auto.ToString().ToLower() - : eCameraControlMode.Manual.ToString().ToLower(); - } - - if (Codec is IHasCameraOff cameraOffCodec) - { - if (cameraOffCodec.CameraIsOffFeedback.BoolValue) - m = eCameraControlMode.Off.ToString().ToLower(); - } - - return m; - } - - private void PostCallHistory() - { - try - { - var codec = (Codec as IHasCallHistory); - - if (codec != null) - { - var status = new VideoCodecBaseStateMessage(); - - var recents = codec.CallHistory.RecentCalls; - - if (recents != null) - { - status.RecentCalls = codec.CallHistory.RecentCalls; - - PostStatusMessage(status); - } - } - } - catch (Exception ex) - { - this.LogError(ex, "Error posting call history"); - } - } - - /// - /// Helper to grab a call with string ID - /// - /// - /// - private CodecActiveCallItem GetCallWithId(string id) - { - return Codec.ActiveCalls.FirstOrDefault(c => c.Id == id); - } - - /// - /// - /// - /// - private void GetDirectory(string id) - { - if (!(Codec is IHasDirectory dirCodec)) - { - return; - } - dirCodec.GetDirectoryFolderContents(id); - } - - /// - /// - /// - private void GetDirectoryRoot() - { - try - { - if (!(Codec is IHasDirectory dirCodec)) - { - // do something else? - return; - } - if (!dirCodec.PhonebookSyncState.InitialSyncComplete) - { - var state = new VideoCodecBaseStateMessage - { - InitialPhonebookSyncComplete = false - }; - - PostStatusMessage(state); - return; - } - - dirCodec.SetCurrentDirectoryToRoot(); - } - catch (Exception ex) - { - this.LogError(ex, "Error getting directory root"); - } - } - - /// - /// Requests the parent folder contents - /// - private void GetPreviousDirectory() - { - if (!(Codec is IHasDirectory dirCodec)) - { - return; - } - - dirCodec.GetDirectoryParentFolderContents(); - } - /// /// Handler for codec changes /// @@ -797,96 +280,32 @@ namespace PepperDash.Essentials.AppServer.Messengers Task.Run(() => PostStatusMessage(GetStatus(), id)); } - private void PostReceivingContent(bool receivingContent) - { - try - { - var state = new VideoCodecBaseStateMessage - { - ReceivingContent = receivingContent - }; - - PostStatusMessage(state); - } - catch (Exception ex) - { - this.LogError(ex, "Error posting receiving content"); - } - } - - private void PostCameraSelfView() - { - try - { - var status = new VideoCodecBaseStateMessage - { - CameraSelfViewIsOn = Codec is IHasCodecSelfView - && (Codec as IHasCodecSelfView).SelfviewIsOnFeedback.BoolValue - }; - - PostStatusMessage(status); - } - catch (Exception ex) - { - this.LogError(ex, "Error posting camera self view"); - } - } - /// - /// + /// Helper to grab a call with string ID /// - private void PostCameraMode() + private CodecActiveCallItem GetCallWithId(string id) { - try - { - var status = new VideoCodecBaseStateMessage - { - CameraMode = GetCameraMode() - }; - - PostStatusMessage(status); - } - catch (Exception ex) - { - this.LogError(ex, "Error posting camera mode"); - } + return Codec.ActiveCalls.FirstOrDefault(c => c.Id == id); } - private void PostSelectedCamera() + private string GetCameraMode() { - try - { - var camerasCodec = Codec as IHasCodecCameras; + string m = ""; - var status = new VideoCodecBaseStateMessage - { - Cameras = new CameraStatus() { SelectedCamera = GetSelectedCamera(camerasCodec) }, - Presets = GetCurrentPresets() - }; - - PostStatusMessage(status); - } - catch (Exception e) + if (Codec is IHasCameraAutoMode speakerTrackCodec) { - this.LogError(e, "Error posting selected camera"); + m = speakerTrackCodec.CameraAutoModeIsOnFeedback.BoolValue + ? eCameraControlMode.Auto.ToString().ToLower() + : eCameraControlMode.Manual.ToString().ToLower(); } - } - private void PostCameraPresets() - { - try + if (Codec is IHasCameraOff cameraOffCodec) { - var status = new VideoCodecBaseStateMessage - { - Presets = GetCurrentPresets() - }; + if (cameraOffCodec.CameraIsOffFeedback.BoolValue) + m = eCameraControlMode.Off.ToString().ToLower(); + } - PostStatusMessage(status); - } - catch (Exception e) - { - this.LogError(e, "Error posting camera presets"); - } + return m; } private Camera GetSelectedCamera(IHasCodecCameras camerasCodec) @@ -1145,45 +564,4 @@ namespace PepperDash.Essentials.AppServer.Messengers public bool? CanFocus { get; set; } } - - /// - /// Represents a VideoCodecBaseEventMessage - /// - public class VideoCodecBaseEventMessage : DeviceEventMessageBase - { - - } - - /// - /// Represents a PasswordPromptEventMessage - /// - public class PasswordPromptEventMessage : VideoCodecBaseEventMessage - { - /// - /// Gets or sets the Message - /// - [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] - public string Message { get; set; } - - - /// - /// Gets or sets the LastAttemptWasIncorrect - /// - [JsonProperty("lastAttemptWasIncorrect", NullValueHandling = NullValueHandling.Ignore)] - public bool LastAttemptWasIncorrect { get; set; } - - - /// - /// Gets or sets the LoginAttemptFailed - /// - [JsonProperty("loginAttemptFailed", NullValueHandling = NullValueHandling.Ignore)] - public bool LoginAttemptFailed { get; set; } - - - /// - /// Gets or sets the LoginAttemptCancelled - /// - [JsonProperty("loginAttemptCancelled", NullValueHandling = NullValueHandling.Ignore)] - public bool LoginAttemptCancelled { get; set; } - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index 060e594f..96bc8ee5 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -10,6 +10,7 @@ using PepperDash.Essentials.Core.Routing; using PepperDash.Essentials.Core.Shades; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Devices.Common.Displays; using PepperDash.Essentials.Devices.Common.SoftCodec; using PepperDash.Essentials.Devices.Common.VideoCodec; @@ -95,7 +96,7 @@ namespace PepperDash.Essentials ), new MessengerFactoryEntry( typeof(IHasCamerasWithControls), - (d, mp, ck) => new IHasCamerasWithControlMessenger( + (d, mp, ck) => new IHasCamerasWithControlsMessenger( $"{d.Key}-cameras-{ck}", mp, (IHasCamerasWithControls)d) ), @@ -164,6 +165,42 @@ namespace PepperDash.Essentials (d, mp, ck) => new VideoCodecBaseMessenger( $"{d.Key}-videoCodec-{ck}", (VideoCodecBase)d, mp) ), + new MessengerFactoryEntry( + typeof(IHasDirectory), + (d, mp, ck) => new IHasDirectoryMessenger( + $"{d.Key}-directory-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasCallHistory), + (d, mp, ck) => new IHasCallHistoryMessenger( + $"{d.Key}-callHistory-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IPasswordPrompt), + (d, mp, ck) => new IPasswordPromptMessenger( + $"{d.Key}-passwordPrompt-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasCodecCameras), + (d, mp, ck) => new IHasCodecCamerasMessenger( + $"{d.Key}-codecCameras-{ck}", mp, (VideoCodecBase)d), + predicate: d => d is VideoCodecBase && d is IHasCodecCameras + ), + new MessengerFactoryEntry( + typeof(IHasCodecSelfView), + (d, mp, ck) => new IHasCodecSelfViewMessenger( + $"{d.Key}-selfView-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasCodecLayouts), + (d, mp, ck) => new IHasCodecLayoutsMessenger( + $"{d.Key}-codecLayouts-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasFarEndContentStatus), + (d, mp, ck) => new IHasFarEndContentStatusMessenger( + $"{d.Key}-farEndContent-{ck}", mp, d) + ), new MessengerFactoryEntry( typeof(IDialerCallStatus), (d, mp, ck) => new IDialerCallStatusMessenger( diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 1539fcf0..701a0ec4 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -9,6 +9,7 @@ using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.Diagnostics; +using Crestron.SimplSharpPro.UC; using PepperDash.Core; using PepperDash.Core.Abstractions; using PepperDash.Core.Adapters; From b41c30cdd02a51664ed6969847f123060dd77476 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 7 May 2026 20:55:44 -0600 Subject: [PATCH 063/161] feat: Add full status actions to IHasCodecSelfView and IHasDirectory messengers --- .../Messengers/IHasCodecSelfViewMessenger.cs | 9 ++++++++ .../Messengers/IHasDirectoryMessenger.cs | 22 +++++++++++++++++++ .../Messengers/VideoCodecBaseMessenger.cs | 17 -------------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs index 885c786d..dd31e547 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs @@ -28,6 +28,10 @@ namespace PepperDash.Essentials.AppServer.Messengers { base.RegisterActions(); + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + AddAction("/cameraSelfViewStatus", (id, content) => SendFullStatus()); + AddAction("/cameraSelfView", (id, content) => _selfView.SelfViewModeToggle()); _selfView.SelfviewIsOnFeedback.OutputChange += SelfviewIsOnFeedback_OutputChange; @@ -38,6 +42,11 @@ namespace PepperDash.Essentials.AppServer.Messengers PostCameraSelfView(); } + private void SendFullStatus() + { + PostCameraSelfView(); + } + private void PostCameraSelfView() { try diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs index 677d31c1..8859a9fc 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs @@ -30,6 +30,10 @@ namespace PepperDash.Essentials.AppServer.Messengers { base.RegisterActions(); + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + AddAction("/directoryStatus", (id, content) => SendFullStatus()); + AddAction("/getDirectory", (id, content) => GetDirectoryRoot()); AddAction("/directoryById", (id, content) => @@ -116,6 +120,17 @@ namespace PepperDash.Essentials.AppServer.Messengers { _directory.GetDirectoryParentFolderContents(); } + + private void SendFullStatus() + { + PostStatusMessage(new IHasDirectoryStateMessage + { + CurrentDirectory = _directory.CurrentDirectoryResult, + InitialPhonebookSyncComplete = _directory.PhonebookSyncState.InitialSyncComplete, + HasDirectory = true, + HasDirectorySearch = true, + }); + } } public class IHasDirectoryStateMessage : DeviceStateMessageBase @@ -125,5 +140,12 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("initialPhonebookSyncComplete", NullValueHandling = NullValueHandling.Ignore)] public bool? InitialPhonebookSyncComplete { get; set; } + + [JsonProperty("hasDirectory", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasDirectory { get; set; } + + [JsonProperty("hasDirectorySearch", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasDirectorySearch { get; set; } + } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs index 285d3f3f..58e5da2f 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs @@ -227,13 +227,6 @@ namespace PepperDash.Essentials.AppServer.Messengers }; } - if (Codec is IHasDirectory directoryCodec) - { - status.HasDirectory = true; - status.HasDirectorySearch = true; - status.CurrentDirectory = directoryCodec.CurrentDirectoryResult; - } - var codecType = Codec.GetType(); status.CameraSelfViewIsOn = Codec is IHasCodecSelfView && (Codec as IHasCodecSelfView).SelfviewIsOnFeedback.BoolValue; @@ -387,12 +380,6 @@ namespace PepperDash.Essentials.AppServer.Messengers public string CurrentDialString { get; set; } - /// - /// Gets or sets the CurrentDirectory - /// - [JsonProperty("currentDirectory", NullValueHandling = NullValueHandling.Ignore)] - public CodecDirectory CurrentDirectory { get; set; } - /// /// Gets or sets the DirectorySelectedFolderName @@ -403,11 +390,7 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("hasCameras", NullValueHandling = NullValueHandling.Ignore)] public bool? HasCameras { get; set; } - [JsonProperty("hasDirectory", NullValueHandling = NullValueHandling.Ignore)] - public bool? HasDirectory { get; set; } - [JsonProperty("hasDirectorySearch", NullValueHandling = NullValueHandling.Ignore)] - public bool? HasDirectorySearch { get; set; } [JsonProperty("hasPresets", NullValueHandling = NullValueHandling.Ignore)] public bool? HasPresets { get; set; } From 91aa0efa5f4efab25ea5ab5bceada8b49f0298ca Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 8 May 2026 12:10:56 -0600 Subject: [PATCH 064/161] feat: Add new messengers for various device interfaces and remove VideoCodecBaseMessenger - Implemented IHasMeetingInfoMessenger to handle meeting information updates. - Created IHasReadyMessenger for devices indicating readiness status. - Added IHasStandbyModeMessenger to manage standby mode actions. - Introduced IPrivacyMessenger for privacy mode control. - Developed IVideoCodecInfoMessenger to provide codec information. - Removed the obsolete VideoCodecBaseMessenger class. - Updated MessengerFactoryRegistry to register new messengers for IHasReady, IHasMeetingInfo, IHasStartMeeting, IHasStandbyMode, IPrivacy, and IVideoCodecInfo interfaces. Co-authored-by: Copilot --- .../Codec/Interfaces/ICodecCallControls.cs | 21 + .../Codec/iHasDialer.cs | 2 + .../VideoCodec/Interfaces/iVideoCodecInfo.cs | 2 +- .../VideoCodec/VideoCodecBase.cs | 9 +- .../Messengers/IHasCallControlsMessenger.cs | 116 ++++ .../Messengers/IHasCallHistoryMessenger.cs | 15 +- .../IHasCamerasWithControlsMessenger.cs | 34 +- .../Messengers/IHasCodecCamerasMessenger.cs | 145 +++++ .../IHasCodecRoomPresetsMessenger.cs | 83 +++ .../Messengers/IHasCodecSelfViewMessenger.cs | 17 +- .../Messengers/IHasContentSharingMessenger.cs | 93 +++ .../Messengers/IHasDialerMessenger.cs | 99 ++++ .../Messengers/IHasDirectoryMessenger.cs | 8 +- .../IHasFarEndContentStatusMessenger.cs | 34 +- .../Messengers/IHasMeetingInfoMessenger.cs | 86 +++ .../Messengers/IHasReadyMessenger.cs | 70 +++ .../Messengers/IHasStandbyModeMessenger.cs | 83 +++ .../Messengers/IHasStartMeetingMessenger.cs | 85 +++ .../Messengers/IPrivacyMessenger.cs | 89 +++ .../Messengers/IVideoCodecInfoMessenger.cs | 71 +++ .../Messengers/VideoCodecBaseMessenger.cs | 550 ------------------ .../MessengerFactoryRegistry.cs | 52 +- 22 files changed, 1197 insertions(+), 567 deletions(-) create mode 100644 src/PepperDash.Essentials.Devices.Common/Codec/Interfaces/ICodecCallControls.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallControlsMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecRoomPresetsMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasContentSharingMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasMeetingInfoMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasReadyMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasStandbyModeMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasStartMeetingMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IPrivacyMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IVideoCodecInfoMessenger.cs delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/Interfaces/ICodecCallControls.cs b/src/PepperDash.Essentials.Devices.Common/Codec/Interfaces/ICodecCallControls.cs new file mode 100644 index 00000000..6110bb1a --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/Codec/Interfaces/ICodecCallControls.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace PepperDash.Essentials.Devices.Common.Codec; + +/// +/// Defines call control functionality for a codec, extending the base dialer interface +/// with active call list access and meeting dialing. +/// +public interface ICodecCallControls : IHasDialer +{ + /// + /// Gets the list of currently active, dialing, or incoming calls + /// + List ActiveCalls { get; } + + /// + /// Dials the specified meeting + /// + /// The meeting to dial + void Dial(Meeting meeting); +} diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/iHasDialer.cs b/src/PepperDash.Essentials.Devices.Common/Codec/iHasDialer.cs index f378e621..95371404 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/iHasDialer.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/iHasDialer.cs @@ -53,5 +53,7 @@ public interface IHasDialer /// Gets a value indicating whether the device is currently in a call /// bool IsInCall { get; } + + } diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/iVideoCodecInfo.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/iVideoCodecInfo.cs index 8b1be9e9..a2dd9e55 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/iVideoCodecInfo.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/iVideoCodecInfo.cs @@ -5,7 +5,7 @@ namespace PepperDash.Essentials.Devices.Common.Codec /// /// Implements a common set of data about a codec /// - public interface iVideoCodecInfo + public interface IVideoCodecInfo { /// /// Gets the codec information diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index 8ed740d3..59f6097b 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro.DeviceSupport; -using Crestron.SimplSharp; using PepperDash.Core; using PepperDash.Core.Intersystem; using PepperDash.Core.Intersystem.Tokens; @@ -29,7 +28,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec; /// Also contains the logic to link commonly implemented interfaces to the API bridge. /// public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs, - IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode + IUsageTracking, ICodecCallControls, IHasContentSharing, ICodecAudio, IVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode, IHasReady { private const int XSigEncoding = 28591; @@ -338,7 +337,7 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu /// /// Fired when the Codec is ready to be used /// - public event EventHandler IsReadyChange; + public event EventHandler IsReadyEvent; /// /// Dials the specified meeting @@ -405,7 +404,7 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu try { IsReady = true; - IsReadyChange?.Invoke(this, new EventArgs()); + IsReadyEvent?.Invoke(this, new IsReadyEventArgs(IsReady)); } catch (Exception e) { @@ -498,7 +497,7 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu LinkVideoCodecInfoToApi(trilist, joinMap); // Register for this event to link any functions that require the codec to be ready first - codec.IsReadyChange += (o, a) => + codec.IsReadyEvent += (o, a) => { if (codec is IHasCodecCameras) { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallControlsMessenger.cs new file mode 100644 index 00000000..c28cbede --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallControlsMessenger.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class ICallControlsMessenger : MessengerBase + { + private readonly ICodecCallControls _callControls; + + /// Initializes a new instance of the class. + public ICallControlsMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _callControls = device as ICodecCallControls ?? throw new ArgumentNullException(nameof(device)); + _callControls.CallStatusChange += CallControls_CallStatusChange; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/callControlsStatus", (id, content) => SendFullStatus(id)); + + AddAction("/dialMeeting", (id, content) => + _callControls.Dial(content.ToObject())); + + AddAction("/endCallById", (id, content) => + { + var s = content.ToObject>(); + var call = GetCallWithId(s.Value); + if (call != null) + _callControls.EndCall(call); + }); + + AddAction("/rejectById", (id, content) => + { + var s = content.ToObject>(); + var call = GetCallWithId(s.Value); + if (call != null) + _callControls.RejectCall(call); + }); + + AddAction("/acceptById", (id, content) => + { + var s = content.ToObject>(); + var call = GetCallWithId(s.Value); + if (call != null) + _callControls.AcceptCall(call); + }); + } + + private void CallControls_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e) + { + try + { + PostStatusMessage(BuildState()); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting call controls status"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + Task.Run(() => PostStatusMessage(BuildState(), id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending call controls full status"); + } + } + + private ICallControlsStateMessage BuildState() + { + return new ICallControlsStateMessage + { + Calls = _callControls.ActiveCalls, + }; + } + + private CodecActiveCallItem GetCallWithId(string id) + { + return _callControls.ActiveCalls?.FirstOrDefault(c => c.Id == id); + } + } + + /// + /// State message for + /// + public class ICallControlsStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets the list of active calls. Null if unknown or not applicable. + /// + [JsonProperty("calls", NullValueHandling = NullValueHandling.Ignore)] + public List Calls { get; set; } + + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs index 10e2f8d6..388f6a3c 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs @@ -65,7 +65,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { PostStatusMessage(new IHasCallHistoryStateMessage { - RecentCalls = recents + RecentCalls = recents, }); } } @@ -76,9 +76,22 @@ namespace PepperDash.Essentials.AppServer.Messengers } } + /// + /// State message for + /// + /// public class IHasCallHistoryStateMessage : DeviceStateMessageBase { + /// + /// Gets or sets the list of recent calls. Null if unknown or not applicable. + /// [JsonProperty("recentCalls", NullValueHandling = NullValueHandling.Ignore)] public List RecentCalls { get; set; } + + /// + /// Gets or sets a value indicating whether the device has call history functionality. Null if unknown or not applicable. + /// + [JsonProperty("hasRecents", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasRecents { get; set; } = true; // Since this messenger should only be used for devices with call history, default to true unless specified otherwise. } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlsMessenger.cs index e20eabdc..cb83a26e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCamerasWithControlsMessenger.cs @@ -81,7 +81,8 @@ namespace PepperDash.Essentials.AppServer.Messengers foreach (var cam in CameraController.Cameras) { - cameraList.Add(new KeyName{ + cameraList.Add(new KeyName + { Key = cam.Key, Name = cam.Name }); @@ -96,10 +97,27 @@ namespace PepperDash.Essentials.AppServer.Messengers }; } + string mode = ""; + + if (CameraController is IHasCameraAutoMode speakerTrackCodec) + { + mode = speakerTrackCodec.CameraAutoModeIsOnFeedback.BoolValue + ? eCameraControlMode.Auto.ToString().ToLower() + : eCameraControlMode.Manual.ToString().ToLower(); + } + + if (CameraController is IHasCameraOff cameraOffCodec) + { + if (cameraOffCodec.CameraIsOffFeedback.BoolValue) + mode = eCameraControlMode.Off.ToString().ToLower(); + } + var state = new IHasCamerasWithControlsStateMessage { CameraList = cameraList, - SelectedCamera = selectedCamera + SelectedCamera = selectedCamera, + CameraMode = mode, + HasCameraAutoMode = CameraController is IHasCameraAutoMode, }; PostStatusMessage(state, clientId); @@ -122,6 +140,18 @@ namespace PepperDash.Essentials.AppServer.Messengers /// [JsonProperty("selectedCamera", NullValueHandling = NullValueHandling.Ignore)] public IKeyName SelectedCamera { get; set; } + + /// + /// Indicates whether the device has any cameras. Null if unknown or not applicable. + /// + [JsonProperty("hasCameras", NullValueHandling = NullValueHandling.Ignore)] + public bool HasCameras { get; set; } = true; // Since this messenger should only be used for devices with cameras, default to true unless specified otherwise. + + [JsonProperty("hasCameraAutoMode", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasCameraAutoMode { get; set; } + + [JsonProperty("cameraMode", NullValueHandling = NullValueHandling.Ignore)] + public string CameraMode { get; set; } } class KeyName : IKeyName diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs index c3f8d3fd..9ea151bc 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs @@ -249,6 +249,35 @@ namespace PepperDash.Essentials.AppServer.Messengers return presetsCodec.NearEndPresets; } + private Camera GetSelectedCamera(IHasCodecCameras camerasCodec) + { + var camera = new Camera(); + + if (camerasCodec.SelectedCameraFeedback != null) + camera.Key = camerasCodec.SelectedCameraFeedback.StringValue; + if (camerasCodec.SelectedCamera != null) + { + camera.Name = camerasCodec.SelectedCamera.Name; + + if(camerasCodec.SelectedCamera is IHasCameraPtzControl cameraControls) + { + camera.Capabilities = new CameraCapabilities() + { + CanPan = cameraControls is IHasCameraPanControl, + CanTilt = cameraControls is IHasCameraTiltControl, + CanZoom = cameraControls is IHasCameraZoomControl, + CanFocus = cameraControls is IHasCameraFocusControl, + }; + }; + } + + if (camerasCodec.ControllingFarEndCameraFeedback != null) + camera.IsFarEnd = camerasCodec.ControllingFarEndCameraFeedback.BoolValue; + + + return camera; + } + private void PostCameraMode() { try @@ -294,6 +323,31 @@ namespace PepperDash.Essentials.AppServer.Messengers this.LogError(ex, "Error posting camera presets"); } } + + private void SendFullStatus() + { + try + { + PostStatusMessage(new IHasCodecCamerasStateMessage + { + CameraMode = GetCameraMode(), + Cameras = new CameraStatus + { + CameraManualIsSupported = true, + CameraAutoIsSupported = _codec.SupportsCameraAutoMode, + CameraOffIsSupported = _codec.SupportsCameraOff, + CameraMode = GetCameraMode(), + Cameras = _cameraCodec.Cameras, + SelectedCamera = GetSelectedCamera(_cameraCodec) + }, + Presets = GetCurrentPresets() + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending full camera status"); + } + } } public class IHasCodecCamerasStateMessage : DeviceStateMessageBase @@ -306,5 +360,96 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)] public List Presets { get; set; } + + [JsonProperty("cameraSupportsAutoMode", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraSupportsAutoMode { get; set; } + + [JsonProperty("cameraSupportsOffMode", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraSupportsOffMode { get; set; } + } + + /// + /// Represents a CameraStatus + /// + public class CameraStatus + { + [JsonProperty("cameraManualSupported", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraManualIsSupported { get; set; } + + [JsonProperty("cameraAutoSupported", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraAutoIsSupported { get; set; } + + [JsonProperty("cameraOffSupported", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraOffIsSupported { get; set; } + + + /// + /// Gets or sets the CameraMode + /// + [JsonProperty("cameraMode", NullValueHandling = NullValueHandling.Ignore)] + public string CameraMode { get; set; } + + + /// + /// Gets or sets the Cameras + /// + [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] + public List Cameras { get; set; } + + + /// + /// Gets or sets the SelectedCamera + /// + [JsonProperty("selectedCamera", NullValueHandling = NullValueHandling.Ignore)] + public Camera SelectedCamera { get; set; } + } + + /// + /// Represents a Camera + /// + public class Camera + { + + /// + /// Gets or sets the Key + /// + [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] + public string Key { get; set; } + + + /// + /// Gets or sets the Name + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("isFarEnd", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsFarEnd { get; set; } + + + /// + /// Gets or sets the Capabilities + /// + [JsonProperty("capabilities", NullValueHandling = NullValueHandling.Ignore)] + public CameraCapabilities Capabilities { get; set; } + } + + /// + /// Represents a CameraCapabilities + /// + public class CameraCapabilities + { + [JsonProperty("canPan", NullValueHandling = NullValueHandling.Ignore)] + public bool? CanPan { get; set; } + + [JsonProperty("canTilt", NullValueHandling = NullValueHandling.Ignore)] + public bool? CanTilt { get; set; } + + [JsonProperty("canZoom", NullValueHandling = NullValueHandling.Ignore)] + public bool? CanZoom { get; set; } + + [JsonProperty("canFocus", NullValueHandling = NullValueHandling.Ignore)] + public bool? CanFocus { get; set; } + } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecRoomPresetsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecRoomPresetsMessenger.cs new file mode 100644 index 00000000..4bcbe254 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecRoomPresetsMessenger.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasCodecRoomPresetsMessenger : MessengerBase + { + private readonly IHasCodecRoomPresets _presets; + private readonly EssentialsDevice _device; + + public IHasCodecRoomPresetsMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _device = device ?? throw new ArgumentNullException(nameof(device)); + _presets = device as IHasCodecRoomPresets ?? throw new ArgumentNullException(nameof(device)); + _presets.CodecRoomPresetsListHasChanged += Presets_ListHasChanged; + } + + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + } + + private void Presets_ListHasChanged(object sender, EventArgs e) + { + try + { + PostStatusMessage(new IHasCodecRoomPresetsStateMessage + { + Presets = GetCurrentPresets() + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting codec room presets"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasCodecRoomPresetsStateMessage + { + Presets = GetCurrentPresets() + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending room presets full status"); + } + } + + private List GetCurrentPresets() + { + if (_device is IHasFarEndCameraControl farEndControl && + farEndControl.ControllingFarEndCameraFeedback.BoolValue) + return _presets.FarEndRoomPresets; + + return _presets.NearEndPresets; + } + } + + public class IHasCodecRoomPresetsStateMessage : DeviceStateMessageBase + { + [JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)] + public List Presets { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs index dd31e547..85e103f7 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs @@ -53,7 +53,8 @@ namespace PepperDash.Essentials.AppServer.Messengers { PostStatusMessage(new IHasCodecSelfViewStateMessage { - CameraSelfViewIsOn = _selfView.SelfviewIsOnFeedback.BoolValue + CameraSelfViewIsOn = _selfView.SelfviewIsOnFeedback.BoolValue, + ShowSelfViewByDefault = _selfView.ShowSelfViewByDefault }); } catch (Exception ex) @@ -63,9 +64,23 @@ namespace PepperDash.Essentials.AppServer.Messengers } } + /// + /// State message for + /// public class IHasCodecSelfViewStateMessage : DeviceStateMessageBase { + /// + /// Gets or sets a value indicating whether the codec's self view is currently on. Null if unknown or not applicable. + /// + /// [JsonProperty("cameraSelfView", NullValueHandling = NullValueHandling.Ignore)] public bool? CameraSelfViewIsOn { get; set; } + + /// + /// Gets or sets a value indicating whether the codec is set to show self view by default. Null if unknown or not applicable. + /// + /// + [JsonProperty("showSelfViewByDefault", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowSelfViewByDefault { get; set; } } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasContentSharingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasContentSharingMessenger.cs new file mode 100644 index 00000000..159c9a02 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasContentSharingMessenger.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasContentSharingMessenger : MessengerBase + { + private readonly IHasContentSharing _sharing; + + public IHasContentSharingMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _sharing = device as IHasContentSharing ?? throw new ArgumentNullException(nameof(device)); + _sharing.SharingContentIsOnFeedback.OutputChange += SharingContentIsOnFeedback_OutputChange; + _sharing.SharingSourceFeedback.OutputChange += SharingSourceFeedback_OutputChange; + } + + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + AddAction("/sharingStart", (id, content) => _sharing.StartSharing()); + AddAction("/sharingStop", (id, content) => _sharing.StopSharing()); + } + + private void SharingContentIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + try + { + PostStatusMessage(new IHasContentSharingStateMessage + { + SharingContentIsOn = e.BoolValue + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting sharing content is on"); + } + } + + private void SharingSourceFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + try + { + PostStatusMessage(new IHasContentSharingStateMessage + { + SharingSource = e.StringValue + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting sharing source"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasContentSharingStateMessage + { + SharingContentIsOn = _sharing.SharingContentIsOnFeedback.BoolValue, + SharingSource = _sharing.SharingSourceFeedback.StringValue + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending content sharing full status"); + } + } + } + + public class IHasContentSharingStateMessage : DeviceStateMessageBase + { + [JsonProperty("sharingContentIsOn", NullValueHandling = NullValueHandling.Ignore)] + public bool? SharingContentIsOn { get; set; } + + [JsonProperty("sharingSource", NullValueHandling = NullValueHandling.Ignore)] + public string SharingSource { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs new file mode 100644 index 00000000..143f953e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasDialerMessenger : MessengerBase + { + private readonly IHasDialer _dialer; + + /// + public IHasDialerMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _dialer = device as IHasDialer ?? throw new ArgumentNullException(nameof(device)); + _dialer.CallStatusChange += Dialer_CallStatusChange; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/dialStatus", (id, content) => SendFullStatus(id)); + + AddAction("/dial", (id, content) => + { + var value = content.ToObject>(); + _dialer.Dial(value.Value); + }); + + + AddAction("/endAllCalls", (id, content) => _dialer.EndAllCalls()); + + AddAction("/dtmf", (id, content) => + { + var s = content.ToObject>(); + _dialer.SendDtmf(s.Value); + }); + } + + private void Dialer_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e) + { + try + { + var state = new IHasDialerStateMessage + { + IsInCall = _dialer.IsInCall + }; + + PostStatusMessage(state); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting dialer call status"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasDialerStateMessage + { + IsInCall = _dialer.IsInCall, + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending dialer full status"); + } + } + } + + /// + /// Message class representing the state of a device implementing + /// + public class IHasDialerStateMessage : DeviceStateMessageBase + { + /// + /// Indicates whether the device is currently in a call + /// + [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsInCall { get; set; } + + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs index 8859a9fc..a19a4ae8 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs @@ -129,7 +129,7 @@ namespace PepperDash.Essentials.AppServer.Messengers InitialPhonebookSyncComplete = _directory.PhonebookSyncState.InitialSyncComplete, HasDirectory = true, HasDirectorySearch = true, - }); + }); } } @@ -147,5 +147,11 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("hasDirectorySearch", NullValueHandling = NullValueHandling.Ignore)] public bool? HasDirectorySearch { get; set; } + /// + /// Gets or sets the DirectorySelectedFolderName + /// + [JsonProperty("directorySelectedFolderName", NullValueHandling = NullValueHandling.Ignore)] + public string DirectorySelectedFolderName { get; set; } + } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasFarEndContentStatusMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasFarEndContentStatusMessenger.cs index 0e307a6f..c736bf82 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasFarEndContentStatusMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasFarEndContentStatusMessenger.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core.Logging; @@ -12,7 +13,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// public class IHasFarEndContentStatusMessenger : MessengerBase { - private readonly IHasFarEndContentStatus _device; + private readonly IHasFarEndContentStatus device; /// /// Initializes a new instance of the class. @@ -20,7 +21,7 @@ namespace PepperDash.Essentials.AppServer.Messengers public IHasFarEndContentStatusMessenger(string key, string messagePath, EssentialsDevice device) : base(key, messagePath, device) { - _device = device as IHasFarEndContentStatus ?? throw new ArgumentException("device must implement IHasFarEndContentStatus", nameof(device)); + this.device = device as IHasFarEndContentStatus ?? throw new ArgumentException("device must implement IHasFarEndContentStatus", nameof(device)); } /// @@ -28,7 +29,11 @@ namespace PepperDash.Essentials.AppServer.Messengers { base.RegisterActions(); - _device.ReceivingContent.OutputChange += (sender, args) => PostReceivingContent(args.BoolValue); + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/farEndContentStatus", (id, content) => SendFullStatus(id)); + + device.ReceivingContent.OutputChange += (sender, args) => PostReceivingContent(args.BoolValue); } private void PostReceivingContent(bool receivingContent) @@ -45,10 +50,33 @@ namespace PepperDash.Essentials.AppServer.Messengers this.LogError(ex, "Error posting receiving content"); } } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasFarEndContentStatusStateMessage + { + ReceivingContent = device.ReceivingContent.BoolValue + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting full status"); + } + } } + /// + /// Message class representing the state of a device implementing + /// public class IHasFarEndContentStatusStateMessage : DeviceStateMessageBase { + /// + /// Indicates whether the device is currently receiving content from the far end + /// [JsonProperty("receivingContent", NullValueHandling = NullValueHandling.Ignore)] public bool? ReceivingContent { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasMeetingInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasMeetingInfoMessenger.cs new file mode 100644 index 00000000..7aeb6765 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasMeetingInfoMessenger.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasMeetingInfoMessenger : MessengerBase + { + private readonly IHasMeetingInfo _meetingInfo; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + public IHasMeetingInfoMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _meetingInfo = device as IHasMeetingInfo ?? throw new ArgumentNullException(nameof(device)); + _meetingInfo.MeetingInfoChanged += MeetingInfo_Changed; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/meetingInfoStatus", (id, content) => SendFullStatus(id)); + } + + private void MeetingInfo_Changed(object sender, MeetingInfoEventArgs e) + { + try + { + PostStatusMessage(new IHasMeetingInfoStateMessage + { + MeetingInfo = _meetingInfo.MeetingInfo + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting meeting info"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasMeetingInfoStateMessage + { + MeetingInfo = _meetingInfo.MeetingInfo + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending meeting info full status"); + } + } + } + + /// + /// Message class for devices implementing + /// + public class IHasMeetingInfoStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets the MeetingInfo + /// + [JsonProperty("meetingInfo", NullValueHandling = NullValueHandling.Ignore)] + public MeetingInfo MeetingInfo { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasReadyMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasReadyMessenger.cs new file mode 100644 index 00000000..3b704087 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasReadyMessenger.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasReadyMessenger : MessengerBase + { + private readonly IHasReady _hasReady; + + public IHasReadyMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _hasReady = device as IHasReady ?? throw new ArgumentNullException(nameof(device)); + _hasReady.IsReadyEvent += HasReady_IsReadyEvent; + } + + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/isReady", (id, content) => SendFullStatus(id)); + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + } + + private void HasReady_IsReadyEvent(object sender, IsReadyEventArgs e) + { + try + { + PostStatusMessage(new IHasReadyStateMessage + { + IsReady = e.IsReady + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting ready state"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasReadyStateMessage + { + IsReady = _hasReady.IsReady + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending ready full status"); + } + } + } + + public class IHasReadyStateMessage : DeviceStateMessageBase + { + [JsonProperty("isReady", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsReady { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasStandbyModeMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasStandbyModeMessenger.cs new file mode 100644 index 00000000..da493d4d --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasStandbyModeMessenger.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasStandbyModeMessenger : MessengerBase + { + private readonly IHasStandbyMode _standby; + + /// + /// Initializes a new instance of the class. + /// + public IHasStandbyModeMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _standby = device as IHasStandbyMode ?? throw new ArgumentNullException(nameof(device)); + _standby.StandbyIsOnFeedback.OutputChange += StandbyIsOnFeedback_OutputChange; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + AddAction("/standbyStatus", (id, content) => SendFullStatus(id)); + + AddAction("/standbyOn", (id, content) => _standby.StandbyActivate()); + AddAction("/standbyOff", (id, content) => _standby.StandbyDeactivate()); + } + + private void StandbyIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + try + { + PostStatusMessage(new IHasStandbyModeStateMessage + { + StandbyIsOn = e.BoolValue + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting standby state"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasStandbyModeStateMessage + { + StandbyIsOn = _standby.StandbyIsOnFeedback.BoolValue + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending standby full status"); + } + } + } + + /// + /// State message for + /// + public class IHasStandbyModeStateMessage : DeviceStateMessageBase + { + /// + /// Indicates whether the device is in standby mode. Null if unknown or not applicable. + /// + [JsonProperty("standbyIsOn", NullValueHandling = NullValueHandling.Ignore)] + public bool? StandbyIsOn { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasStartMeetingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasStartMeetingMessenger.cs new file mode 100644 index 00000000..bc8559c0 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasStartMeetingMessenger.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasStartMeetingMessenger : MessengerBase + { + private readonly IHasStartMeeting _startMeeting; + + /// + /// Initializes a new instance of the class. + /// + /// The key for the messenger. + /// The message path for the messenger. + /// The device implementing . + /// Thrown if the device does not implement . + public IHasStartMeetingMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _startMeeting = device as IHasStartMeeting ?? throw new ArgumentNullException(nameof(device)); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/startMeetingStatus", (id, content) => SendFullStatus(id)); + + AddAction("/startMeeting", (id, content) => + { + var msg = content.ToObject>(); + _startMeeting.StartMeeting(msg?.Value ?? _startMeeting.DefaultMeetingDurationMin); + }); + + AddAction("/leaveMeeting", (id, content) => _startMeeting.LeaveMeeting()); + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasStartMeetingStateMessage + { + DefaultMeetingDurationMin = _startMeeting.DefaultMeetingDurationMin + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending start meeting full status"); + } + } + } + + /// + /// State message for devices implementing + /// + public class IHasStartMeetingStateMessage : DeviceStateMessageBase + { + /// + /// Indicates whether the device supports ad-hoc meetings (meetings started from the device rather than an external calendar invite) + /// + /// + [JsonProperty("supportsAdHocMeeting", NullValueHandling = NullValueHandling.Ignore)] + public bool SupportsAdHocMeeting { get; set; } = true; + + /// + /// The default meeting duration in minutes for meetings started from the device. Null if unknown or not applicable. + /// + [JsonProperty("defaultMeetingDurationMin", NullValueHandling = NullValueHandling.Ignore)] + public uint DefaultMeetingDurationMin { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IPrivacyMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IPrivacyMessenger.cs new file mode 100644 index 00000000..7657606e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IPrivacyMessenger.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IPrivacyMessenger : MessengerBase + { + private readonly IPrivacy _privacy; + + /// + /// Initializes a new instance of the class. + /// + /// The key for the messenger. + /// The message path for the messenger. + /// The device implementing . + public IPrivacyMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _privacy = device as IPrivacy ?? throw new ArgumentNullException(nameof(device)); + _privacy.PrivacyModeIsOnFeedback.OutputChange += PrivacyModeIsOnFeedback_OutputChange; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + AddAction("/privacyStatus", (id, content) => SendFullStatus(id)); + + AddAction("/privacyModeOn", (id, content) => _privacy.PrivacyModeOn()); + AddAction("/privacyModeOff", (id, content) => _privacy.PrivacyModeOff()); + AddAction("/privacyModeToggle", (id, content) => _privacy.PrivacyModeToggle()); + } + + private void PrivacyModeIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + try + { + PostStatusMessage(new IPrivacyStateMessage + { + PrivacyModeIsOn = e.BoolValue + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting privacy mode state"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IPrivacyStateMessage + { + PrivacyModeIsOn = _privacy.PrivacyModeIsOnFeedback.BoolValue + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending privacy full status"); + } + } + } + + /// + /// State message for + /// + public class IPrivacyStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets a value indicating whether privacy mode is on. Null if unknown or not + /// applicable. + /// + [JsonProperty("privacyModeIsOn", NullValueHandling = NullValueHandling.Ignore)] + public bool? PrivacyModeIsOn { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IVideoCodecInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IVideoCodecInfoMessenger.cs new file mode 100644 index 00000000..8a8d51fa --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IVideoCodecInfoMessenger.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IVideoCodecInfoMessenger : MessengerBase + { + private readonly IVideoCodecInfo _codecInfo; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + public IVideoCodecInfoMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _codecInfo = device as IVideoCodecInfo ?? throw new ArgumentNullException(nameof(device)); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/codecInfoStatus", (id, content) => SendFullStatus(id)); + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new iVideoCodecInfoStateMessage + { + Info = _codecInfo.CodecInfo + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending codec info full status"); + } + } + } + + /// + /// State message for + /// + public class iVideoCodecInfoStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets the codec information. Null if unknown or not applicable. + /// + [JsonProperty("info", NullValueHandling = NullValueHandling.Ignore)] + public VideoCodecInfo Info { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs deleted file mode 100644 index 58e5da2f..00000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs +++ /dev/null @@ -1,550 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Crestron.SimplSharp; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using PepperDash.Core; -using PepperDash.Core.Logging; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using PepperDash.Essentials.Devices.Common.Cameras; -using PepperDash.Essentials.Devices.Common.Codec; -using PepperDash.Essentials.Devices.Common.VideoCodec; -using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; - -namespace PepperDash.Essentials.AppServer.Messengers -{ - /// - /// Provides a messaging bridge for a VideoCodecBase device - /// - public class VideoCodecBaseMessenger : MessengerBase - { - /// - /// - /// - protected VideoCodecBase Codec { get; private set; } - - /// - /// - /// - /// - /// - /// - public VideoCodecBaseMessenger(string key, VideoCodecBase codec, string messagePath) - : base(key, messagePath, codec) - { - Codec = codec ?? throw new ArgumentNullException("codec"); - codec.CallStatusChange += Codec_CallStatusChange; - codec.IsReadyChange += Codec_IsReadyChange; - } - - /// - /// - /// - /// - /// - private void Codec_IsReadyChange(object sender, EventArgs e) - { - try - { - var state = new VideoCodecBaseStateMessage - { - IsReady = true - }; - - PostStatusMessage(state); - - SendFullStatus(); - } - catch (Exception ex) - { - this.LogError(ex, "Error sending codec ready status"); - } - } - - /// - /// Called from base's RegisterWithAppServer method - /// - protected override void RegisterActions() - { - try - { - base.RegisterActions(); - - AddAction("/isReady", (id, content) => SendIsReady()); - - AddAction("/fullStatus", (id, content) => SendFullStatus(id)); - AddAction("/codecStatus", (id, content) => SendFullStatus(id)); - - AddAction("/dial", (id, content) => - { - var value = content.ToObject>(); - - Codec.Dial(value.Value); - }); - - AddAction("/dialMeeting", (id, content) => Codec.Dial(content.ToObject())); - - AddAction("/endCallById", (id, content) => - { - var s = content.ToObject>(); - var call = GetCallWithId(s.Value); - if (call != null) - Codec.EndCall(call); - }); - - AddAction("/endAllCalls", (id, content) => Codec.EndAllCalls()); - - AddAction("/dtmf", (id, content) => - { - var s = content.ToObject>(); - Codec.SendDtmf(s.Value); - }); - - AddAction("/rejectById", (id, content) => - { - var s = content.ToObject>(); - - var call = GetCallWithId(s.Value); - if (call != null) - Codec.RejectCall(call); - }); - - AddAction("/acceptById", (id, content) => - { - var s = content.ToObject>(); - - var call = GetCallWithId(s.Value); - if (call != null) - Codec.AcceptCall(call); - }); - - Codec.SharingContentIsOnFeedback.OutputChange += SharingContentIsOnFeedback_OutputChange; - Codec.SharingSourceFeedback.OutputChange += SharingSourceFeedback_OutputChange; - - this.LogVerbose("Adding Privacy & Standby Actions"); - - AddAction("/privacyModeOn", (id, content) => Codec.PrivacyModeOn()); - AddAction("/privacyModeOff", (id, content) => Codec.PrivacyModeOff()); - AddAction("/privacyModeToggle", (id, content) => Codec.PrivacyModeToggle()); - AddAction("/sharingStart", (id, content) => Codec.StartSharing()); - AddAction("/sharingStop", (id, content) => Codec.StopSharing()); - AddAction("/standbyOn", (id, content) => Codec.StandbyActivate()); - AddAction("/standbyOff", (id, content) => Codec.StandbyDeactivate()); - } - catch (Exception e) - { - this.LogException(e, "Exception adding paths"); - } - } - - private void SharingSourceFeedback_OutputChange(object sender, FeedbackEventArgs e) - { - try - { - var state = new VideoCodecBaseStateMessage - { - SharingSource = e.StringValue - }; - - PostStatusMessage(state); - } - catch (Exception ex) - { - this.LogError(ex, "Error posting sharing source"); - } - } - - private void SharingContentIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) - { - try - { - var state = new VideoCodecBaseStateMessage - { - SharingContentIsOn = e.BoolValue - }; - - PostStatusMessage(state); - } - catch (Exception ex) - { - this.LogError(ex, "Error posting sharing content"); - } - } - - /// - /// Handler for codec changes - /// - private void Codec_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e) - { - SendFullStatus(); - } - - /// - /// - /// - private void SendIsReady() - { - try - { - var status = new VideoCodecBaseStateMessage(); - - var codecType = Codec.GetType(); - - status.IsReady = Codec.IsReady; - status.IsZoomRoom = codecType.GetInterface("IHasZoomRoomLayouts") != null; - - PostStatusMessage(status); - } - catch (Exception ex) - { - this.LogError(ex, "Error sending codec ready status"); - } - } - - /// - /// Helper method to build call status for vtc - /// - /// - protected VideoCodecBaseStateMessage GetStatus() - { - try - { - var status = new VideoCodecBaseStateMessage(); - - if (Codec is IHasCodecCameras camerasCodec) - { - status.Cameras = new CameraStatus - { - CameraManualIsSupported = true, - CameraAutoIsSupported = Codec.SupportsCameraAutoMode, - CameraOffIsSupported = Codec.SupportsCameraOff, - CameraMode = GetCameraMode(), - Cameras = camerasCodec.Cameras, - SelectedCamera = GetSelectedCamera(camerasCodec) - }; - } - - var codecType = Codec.GetType(); - - status.CameraSelfViewIsOn = Codec is IHasCodecSelfView && (Codec as IHasCodecSelfView).SelfviewIsOnFeedback.BoolValue; - status.IsInCall = Codec.IsInCall; - status.PrivacyModeIsOn = Codec.PrivacyModeIsOnFeedback.BoolValue; - status.SharingContentIsOn = Codec.SharingContentIsOnFeedback.BoolValue; - status.SharingSource = Codec.SharingSourceFeedback.StringValue; - status.StandbyIsOn = Codec.StandbyIsOnFeedback.BoolValue; - status.Calls = Codec.ActiveCalls; - status.Info = Codec.CodecInfo; - status.ShowSelfViewByDefault = Codec.ShowSelfViewByDefault; - status.SupportsAdHocMeeting = Codec is IHasStartMeeting; - status.HasRecents = Codec is IHasCallHistory; - status.HasCameras = Codec is IHasCamerasWithControls; - status.Presets = GetCurrentPresets(); - status.IsZoomRoom = codecType.GetInterface("IHasZoomRoomLayouts") != null; - status.ReceivingContent = Codec is IHasFarEndContentStatus && (Codec as IHasFarEndContentStatus).ReceivingContent.BoolValue; - - if (Codec is IHasMeetingInfo meetingInfoCodec) - { - status.MeetingInfo = meetingInfoCodec.MeetingInfo; - } - - return status; - } - catch (Exception ex) - { - this.LogError(ex, "Error getting codec status"); - return null; - } - } - - /// - /// Sends the full status of the codec, including active calls, camera status, and directory info if applicable - /// - /// - protected virtual void SendFullStatus(string id = null) - { - if (!Codec.IsReady) - { - return; - } - - Task.Run(() => PostStatusMessage(GetStatus(), id)); - } - - /// - /// Helper to grab a call with string ID - /// - private CodecActiveCallItem GetCallWithId(string id) - { - return Codec.ActiveCalls.FirstOrDefault(c => c.Id == id); - } - - private string GetCameraMode() - { - string m = ""; - - if (Codec is IHasCameraAutoMode speakerTrackCodec) - { - m = speakerTrackCodec.CameraAutoModeIsOnFeedback.BoolValue - ? eCameraControlMode.Auto.ToString().ToLower() - : eCameraControlMode.Manual.ToString().ToLower(); - } - - if (Codec is IHasCameraOff cameraOffCodec) - { - if (cameraOffCodec.CameraIsOffFeedback.BoolValue) - m = eCameraControlMode.Off.ToString().ToLower(); - } - - return m; - } - - private Camera GetSelectedCamera(IHasCodecCameras camerasCodec) - { - var camera = new Camera(); - - if (camerasCodec.SelectedCameraFeedback != null) - camera.Key = camerasCodec.SelectedCameraFeedback.StringValue; - if (camerasCodec.SelectedCamera != null) - { - camera.Name = camerasCodec.SelectedCamera.Name; - - if(camerasCodec.SelectedCamera is IHasCameraPtzControl cameraControls) - { - camera.Capabilities = new CameraCapabilities() - { - CanPan = cameraControls is IHasCameraPanControl, - CanTilt = cameraControls is IHasCameraTiltControl, - CanZoom = cameraControls is IHasCameraZoomControl, - CanFocus = cameraControls is IHasCameraFocusControl, - }; - }; - } - - if (camerasCodec.ControllingFarEndCameraFeedback != null) - camera.IsFarEnd = camerasCodec.ControllingFarEndCameraFeedback.BoolValue; - - - return camera; - } - - private List GetCurrentPresets() - { - var presetsCodec = Codec as IHasCodecRoomPresets; - - List currentPresets = null; - - if (presetsCodec != null && Codec is IHasFarEndCameraControl && - (Codec as IHasFarEndCameraControl).ControllingFarEndCameraFeedback.BoolValue) - currentPresets = presetsCodec.FarEndRoomPresets; - else if (presetsCodec != null) currentPresets = presetsCodec.NearEndPresets; - - return currentPresets; - } - } - - /// - /// Represents a VideoCodecBaseStateMessage - /// - public class VideoCodecBaseStateMessage : DeviceStateMessageBase - { - - [JsonProperty("calls", NullValueHandling = NullValueHandling.Ignore)] - public List Calls { get; set; } - - [JsonProperty("cameraMode", NullValueHandling = NullValueHandling.Ignore)] - public string CameraMode { get; set; } - - [JsonProperty("cameraSelfView", NullValueHandling = NullValueHandling.Ignore)] - public bool? CameraSelfViewIsOn { get; set; } - - [JsonProperty("cameras", NullValueHandling = NullValueHandling.Ignore)] - /// - /// Gets or sets the Cameras - /// - public CameraStatus Cameras { get; set; } - - [JsonProperty("cameraSupportsAutoMode", NullValueHandling = NullValueHandling.Ignore)] - public bool? CameraSupportsAutoMode { get; set; } - - [JsonProperty("cameraSupportsOffMode", NullValueHandling = NullValueHandling.Ignore)] - public bool? CameraSupportsOffMode { get; set; } - - - /// - /// Gets or sets the CurrentDialString - /// - [JsonProperty("currentDialString", NullValueHandling = NullValueHandling.Ignore)] - public string CurrentDialString { get; set; } - - - - /// - /// Gets or sets the DirectorySelectedFolderName - /// - [JsonProperty("directorySelectedFolderName", NullValueHandling = NullValueHandling.Ignore)] - public string DirectorySelectedFolderName { get; set; } - - [JsonProperty("hasCameras", NullValueHandling = NullValueHandling.Ignore)] - public bool? HasCameras { get; set; } - - - - [JsonProperty("hasPresets", NullValueHandling = NullValueHandling.Ignore)] - public bool? HasPresets { get; set; } - - [JsonProperty("hasRecents", NullValueHandling = NullValueHandling.Ignore)] - public bool? HasRecents { get; set; } - - [JsonProperty("initialPhonebookSyncComplete", NullValueHandling = NullValueHandling.Ignore)] - public bool? InitialPhonebookSyncComplete { get; set; } - - - /// - /// Gets or sets the Info - /// - [JsonProperty("info", NullValueHandling = NullValueHandling.Ignore)] - public VideoCodecInfo Info { get; set; } - - [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsInCall { get; set; } - - [JsonProperty("isReady", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsReady { get; set; } - - [JsonProperty("isZoomRoom", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsZoomRoom { get; set; } - - - /// - /// Gets or sets the MeetingInfo - /// - [JsonProperty("meetingInfo", NullValueHandling = NullValueHandling.Ignore)] - public MeetingInfo MeetingInfo { get; set; } - - - /// - /// Gets or sets the Presets - /// - [JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)] - public List Presets { get; set; } - - [JsonProperty("privacyModeIsOn", NullValueHandling = NullValueHandling.Ignore)] - public bool? PrivacyModeIsOn { get; set; } - - [JsonProperty("receivingContent", NullValueHandling = NullValueHandling.Ignore)] - public bool? ReceivingContent { get; set; } - - [JsonProperty("recentCalls", NullValueHandling = NullValueHandling.Ignore)] - public List RecentCalls { get; set; } - - [JsonProperty("sharingContentIsOn", NullValueHandling = NullValueHandling.Ignore)] - public bool? SharingContentIsOn { get; set; } - - - /// - /// Gets or sets the SharingSource - /// - [JsonProperty("sharingSource", NullValueHandling = NullValueHandling.Ignore)] - public string SharingSource { get; set; } - - [JsonProperty("showCamerasWhenNotInCall", NullValueHandling = NullValueHandling.Ignore)] - public bool? ShowCamerasWhenNotInCall { get; set; } - - [JsonProperty("showSelfViewByDefault", NullValueHandling = NullValueHandling.Ignore)] - public bool? ShowSelfViewByDefault { get; set; } - - [JsonProperty("standbyIsOn", NullValueHandling = NullValueHandling.Ignore)] - public bool? StandbyIsOn { get; set; } - - [JsonProperty("supportsAdHocMeeting", NullValueHandling = NullValueHandling.Ignore)] - public bool? SupportsAdHocMeeting { get; set; } - } - - /// - /// Represents a CameraStatus - /// - public class CameraStatus - { - [JsonProperty("cameraManualSupported", NullValueHandling = NullValueHandling.Ignore)] - public bool? CameraManualIsSupported { get; set; } - - [JsonProperty("cameraAutoSupported", NullValueHandling = NullValueHandling.Ignore)] - public bool? CameraAutoIsSupported { get; set; } - - [JsonProperty("cameraOffSupported", NullValueHandling = NullValueHandling.Ignore)] - public bool? CameraOffIsSupported { get; set; } - - - /// - /// Gets or sets the CameraMode - /// - [JsonProperty("cameraMode", NullValueHandling = NullValueHandling.Ignore)] - public string CameraMode { get; set; } - - - /// - /// Gets or sets the Cameras - /// - [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] - public List Cameras { get; set; } - - - /// - /// Gets or sets the SelectedCamera - /// - [JsonProperty("selectedCamera", NullValueHandling = NullValueHandling.Ignore)] - public Camera SelectedCamera { get; set; } - } - - /// - /// Represents a Camera - /// - public class Camera - { - - /// - /// Gets or sets the Key - /// - [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] - public string Key { get; set; } - - - /// - /// Gets or sets the Name - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - [JsonProperty("isFarEnd", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsFarEnd { get; set; } - - - /// - /// Gets or sets the Capabilities - /// - [JsonProperty("capabilities", NullValueHandling = NullValueHandling.Ignore)] - public CameraCapabilities Capabilities { get; set; } - } - - /// - /// Represents a CameraCapabilities - /// - public class CameraCapabilities - { - [JsonProperty("canPan", NullValueHandling = NullValueHandling.Ignore)] - public bool? CanPan { get; set; } - - [JsonProperty("canTilt", NullValueHandling = NullValueHandling.Ignore)] - public bool? CanTilt { get; set; } - - [JsonProperty("canZoom", NullValueHandling = NullValueHandling.Ignore)] - public bool? CanZoom { get; set; } - - [JsonProperty("canFocus", NullValueHandling = NullValueHandling.Ignore)] - public bool? CanFocus { get; set; } - - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index 96bc8ee5..ae757962 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -14,6 +14,7 @@ using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Devices.Common.Displays; using PepperDash.Essentials.Devices.Common.SoftCodec; using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; using PepperDash.Essentials.Room.MobileControl; namespace PepperDash.Essentials @@ -161,9 +162,39 @@ namespace PepperDash.Essentials // ── Codecs ─────────────────────────────────────────────────────────────── new MessengerFactoryEntry( - typeof(VideoCodecBase), - (d, mp, ck) => new VideoCodecBaseMessenger( - $"{d.Key}-videoCodec-{ck}", (VideoCodecBase)d, mp) + typeof(IHasReady), + (d, mp, ck) => new IHasReadyMessenger( + $"{d.Key}-ready-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasDialer), + (d, mp, ck) => new IHasDialerMessenger( + $"{d.Key}-dialer-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(ICodecCallControls), + (d, mp, ck) => new ICallControlsMessenger( + $"{d.Key}-callControls-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasContentSharing), + (d, mp, ck) => new IHasContentSharingMessenger( + $"{d.Key}-contentSharing-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasStandbyMode), + (d, mp, ck) => new IHasStandbyModeMessenger( + $"{d.Key}-standby-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IPrivacy), + (d, mp, ck) => new IPrivacyMessenger( + $"{d.Key}-privacy-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IVideoCodecInfo), + (d, mp, ck) => new IVideoCodecInfoMessenger( + $"{d.Key}-codecInfo-{ck}", mp, d) ), new MessengerFactoryEntry( typeof(IHasDirectory), @@ -186,6 +217,11 @@ namespace PepperDash.Essentials $"{d.Key}-codecCameras-{ck}", mp, (VideoCodecBase)d), predicate: d => d is VideoCodecBase && d is IHasCodecCameras ), + new MessengerFactoryEntry( + typeof(IHasCodecRoomPresets), + (d, mp, ck) => new IHasCodecRoomPresetsMessenger( + $"{d.Key}-codecRoomPresets-{ck}", mp, d) + ), new MessengerFactoryEntry( typeof(IHasCodecSelfView), (d, mp, ck) => new IHasCodecSelfViewMessenger( @@ -201,6 +237,16 @@ namespace PepperDash.Essentials (d, mp, ck) => new IHasFarEndContentStatusMessenger( $"{d.Key}-farEndContent-{ck}", mp, d) ), + new MessengerFactoryEntry( + typeof(IHasMeetingInfo), + (d, mp, ck) => new IHasMeetingInfoMessenger( + $"{d.Key}-meetingInfo-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasStartMeeting), + (d, mp, ck) => new IHasStartMeetingMessenger( + $"{d.Key}-startMeeting-{ck}", mp, d) + ), new MessengerFactoryEntry( typeof(IDialerCallStatus), (d, mp, ck) => new IDialerCallStatusMessenger( From b9dcec587df6d872ebc434ee75d7eb0fcb561984 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 13 May 2026 17:00:23 -0600 Subject: [PATCH 065/161] feat: Add new status actions for call history, codec cameras, room presets, content sharing, room event schedule, shutdown prompt, and tech password messengers --- .../Messengers/IHasCallHistoryMessenger.cs | 1 + .../Messengers/IHasCodecCamerasMessenger.cs | 5 ++++- .../Messengers/IHasCodecRoomPresetsMessenger.cs | 1 + .../Messengers/IHasContentSharingMessenger.cs | 1 + .../Messengers/IRoomEventScheduleMessenger.cs | 7 ++++++- .../Messengers/IShutdownPromptTimerMessenger.cs | 3 +-- .../Messengers/ITechPasswordMessenger.cs | 3 +-- .../MessengerFactoryRegistry.cs | 2 -- .../RoomBridges/MobileControlEssentialsRoomBridge.cs | 2 +- 9 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs index 388f6a3c..52bf9cce 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs @@ -31,6 +31,7 @@ namespace PepperDash.Essentials.AppServer.Messengers base.RegisterActions(); AddAction("/fullStatus", (id, content) => PostCallHistory()); + AddAction("/callHistoryStatus", (id, content) => PostCallHistory()); AddAction("/getCallHistory", (id, content) => PostCallHistory()); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs index 9ea151bc..75e0c2e0 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs @@ -27,6 +27,8 @@ namespace PepperDash.Essentials.AppServer.Messengers { _codec = codec ?? throw new ArgumentNullException(nameof(codec)); _cameraCodec = codec as IHasCodecCameras ?? throw new ArgumentException("codec must implement IHasCodecCameras", nameof(codec)); + + _cameraCodec.CameraSelected += CameraCodec_CameraSelected; } /// @@ -38,7 +40,8 @@ namespace PepperDash.Essentials.AppServer.Messengers _cameraCodec.CameraSelected += CameraCodec_CameraSelected; - AddAction("/fullStatus", (id, content) => PostSelectedCamera()); + AddAction("/fullStatus", (id, content) => SendFullStatus()); + AddAction("/codecCamerasStatus", (id, content) => SendFullStatus()); AddAction("/cameraSelect", (id, content) => { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecRoomPresetsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecRoomPresetsMessenger.cs index 4bcbe254..608b24b7 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecRoomPresetsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecRoomPresetsMessenger.cs @@ -31,6 +31,7 @@ namespace PepperDash.Essentials.AppServer.Messengers base.RegisterActions(); AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + AddAction("/roomPresetsStatus", (id, content) => SendFullStatus(id)); } private void Presets_ListHasChanged(object sender, EventArgs e) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasContentSharingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasContentSharingMessenger.cs index 159c9a02..69ebadb0 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasContentSharingMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasContentSharingMessenger.cs @@ -29,6 +29,7 @@ namespace PepperDash.Essentials.AppServer.Messengers base.RegisterActions(); AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + AddAction("/contentSharingStatus", (id, content) => SendFullStatus(id)); AddAction("/sharingStart", (id, content) => _sharing.StartSharing()); AddAction("/sharingStop", (id, content) => _sharing.StopSharing()); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoomEventScheduleMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoomEventScheduleMessenger.cs index 1112a85d..8d5322a0 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoomEventScheduleMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoomEventScheduleMessenger.cs @@ -33,7 +33,12 @@ public class IRoomEventScheduleMessenger : MessengerBase protected override void RegisterActions() { AddAction("/saveScheduledEvents", (id, content) => SaveScheduledEvents(content.ToObject>())); - AddAction("/status", (id, content) => + AddAction("/fullStatus", (id, content) => + { + var events = _room.GetScheduledEvents(); + SendFullStatus(events, id); + }); + AddAction("/roomEventStatus", (id, content) => { var events = _room.GetScheduledEvents(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs index f3b21d7b..73872b54 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs @@ -27,8 +27,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// protected override void RegisterActions() { - AddAction("/status", (id, content) => SendFullStatus(id)); - + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); AddAction("/shutdownPromptStatus", (id, content) => SendFullStatus(id)); AddAction("/setShutdownPromptSeconds", (id, content) => diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs index c511c68c..a2d49cde 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs @@ -27,8 +27,7 @@ namespace PepperDash.Essentials.AppServer.Messengers protected override void RegisterActions() { - AddAction("/status", (id, content) => SendFullStatus(id)); - + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); AddAction("/techPasswordStatus", (id, content) => SendFullStatus(id)); AddAction("/validateTechPassword", (id, content) => diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index ae757962..6caae7b2 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -11,8 +11,6 @@ using PepperDash.Essentials.Core.Shades; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Cameras; using PepperDash.Essentials.Devices.Common.Codec; -using PepperDash.Essentials.Devices.Common.Displays; -using PepperDash.Essentials.Devices.Common.SoftCodec; using PepperDash.Essentials.Devices.Common.VideoCodec; using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; using PepperDash.Essentials.Room.MobileControl; diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs index 93924b24..b6aca416 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -107,7 +107,7 @@ namespace PepperDash.Essentials.RoomBridges // Source Changes and room off - AddAction("/status", (id, content) => + AddAction("/fullStatus", (id, content) => { SendFullStatusForClientId(id, Room); }); From f64b595fd74c0b212cc51a7a5b55d869a9d9a129 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 14 May 2026 14:36:30 -0600 Subject: [PATCH 066/161] feat: Refactor routing interfaces and feedback mechanisms - Removed ITxRouting and ITxRoutingWithFeedback interfaces as they were redundant. - Updated RouteDescriptor and RouteSwitchDescriptor to use new feedback interfaces. - Modified RoutingFeedbackManager to utilize IRoutingSinkWithFeedback and IRoutingMidpointWithFeedback. - Adjusted GenericAudioOut, BlueJeansPc, and other device classes to implement new feedback interfaces. - Introduced new messenger classes for IRoutingSinkWithFeedback and IRoutingMidpointWithFeedback. - Cleaned up unused messenger interfaces and consolidated routing logic. - Updated MobileControlEssentialsRoomBridge to remove dependency on IHasCurrentSourceInfoChange. --- .../Comm and IR/CommFactory.cs | 4 +- .../DeviceTypeInterfaces/IAudioZones.cs | 2 +- .../DeviceTypeInterfaces/IDisplay.cs | 2 +- .../Devices/DestinationListItem.cs | 6 +- .../Room/Interfaces.cs | 2 +- .../Routing/Extensions.cs | 18 +- .../Routing/IHasCurrentSourceInfoChange.cs | 35 --- .../Routing/IMatrixRouting.cs | 28 -- .../Routing/IRmcRouting.cs | 13 - .../Routing/IRmcRoutingWithFeedback.cs | 8 - .../Routing/IRouting.cs | 21 -- .../Routing/IRoutingFeedback.cs | 15 - .../Routing/IRoutingInputSlot.cs | 14 - .../Routing/IRoutingInputsOutputs.cs | 9 - .../Routing/IRoutingMidpoint.cs | 10 + .../Routing/IRoutingMidpointWithFeedback.cs | 43 +++ .../Routing/IRoutingNumeric.cs | 16 -- .../Routing/IRoutingNumericWithFeedback.cs | 8 - .../Routing/IRoutingOutputSlot.cs | 27 -- .../Routing/IRoutingSink.cs | 24 -- .../Routing/IRoutingSinkWithFeedback.cs | 33 ++- .../Routing/IRoutingSinkWithSwitching.cs | 30 -- .../Routing/IRoutingSlot.cs | 20 -- .../Routing/IRoutingWithClear.cs | 14 - .../Routing/IRoutingWithFeedback.cs | 29 -- .../Routing/ITxRouting.cs | 17 -- .../Routing/ITxRoutingWithFeedback.cs | 8 - .../Routing/RouteDescriptor.cs | 8 +- .../Routing/RouteSwitchDescriptor.cs | 2 +- .../Routing/RoutingFeedbackManager.cs | 28 +- .../GetRoutingDevicesAndTieLinesHandler.cs | 4 +- .../Audio/GenericAudioOut.cs | 7 +- .../Displays/BasicIrDisplay.cs | 2 +- .../Displays/MockDisplay.cs | 2 +- .../Displays/TwoWayDisplayBase.cs | 2 +- .../Generic/GenericSink.cs | 32 +-- .../SoftCodec/BlueJeansPc.cs | 60 +--- .../SoftCodec/GenericSoftCodec.cs | 39 +-- .../VideoCodec/VideoCodecBase.cs | 4 +- .../IRoutingSinkWithFeedbackMessenger.cs | 72 +++++ .../IRoutingSinkWithSwitchingMessenger.cs | 8 +- .../IHasCurrentSourceInfoMessenger.cs | 76 ----- .../Messengers/IMatrixRoutingMessenger.cs | 267 ------------------ .../IRoutingMidpointWithFeedbackMessenger.cs | 95 +++++++ .../MessengerFactoryRegistry.cs | 12 +- .../MobileControlEssentialsRoomBridge.cs | 20 +- 46 files changed, 324 insertions(+), 872 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/Routing/IHasCurrentSourceInfoChange.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IMatrixRouting.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRmcRouting.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRmcRoutingWithFeedback.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRouting.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingFeedback.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingInputSlot.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingInputsOutputs.cs create mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingMidpoint.cs create mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingMidpointWithFeedback.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingNumeric.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingNumericWithFeedback.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingOutputSlot.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithSwitching.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingSlot.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingWithClear.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingWithFeedback.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/ITxRouting.cs delete mode 100644 src/PepperDash.Essentials.Core/Routing/ITxRoutingWithFeedback.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithFeedbackMessenger.cs delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoutingMidpointWithFeedbackMessenger.cs diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs index c6a10622..82c5ce6c 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs @@ -155,10 +155,10 @@ namespace PepperDash.Essentials.Core; } - var inputsOutputs = dev as IRoutingInputsOutputs; + var inputsOutputs = dev as IRoutingMidpoint; if (inputsOutputs == null) { - Debug.LogMessage(LogEventLevel.Information, "GetCecPort: Device '{0}' does not support IRoutingInputsOutputs, failed to get CEC port called '{1}'", + Debug.LogMessage(LogEventLevel.Information, "GetCecPort: Device '{0}' does not support IRoutingMidpoint, failed to get CEC port called '{1}'", config.ControlPortDevKey, config.ControlPortName); return null; diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZones.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZones.cs index 540b7a03..cf7fa623 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZones.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAudioZones.cs @@ -5,7 +5,7 @@ namespace PepperDash.Essentials.Core /// /// Identifies a device that contains audio zones /// - public interface IAudioZones : IRouting + public interface IAudioZones : IRoutingMidpointWithFeedback { /// /// Gets the collection of audio zones diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs index 3f38d4e4..2acceb89 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs @@ -9,6 +9,6 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; /// It is designed to be implemented by devices that require these capabilities, /// such as projectors, displays, and other visual output devices. /// -public interface IDisplay : IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName +public interface IDisplay : IHasFeedback, IRoutingSinkWithFeedback, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName { } diff --git a/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs b/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs index 3e61bd80..9ec918a9 100644 --- a/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs @@ -18,16 +18,16 @@ public class DestinationListItem [JsonProperty("sinkKey")] public string SinkKey { get; set; } - private IRoutingSink _sinkDevice; + private IRoutingSinkWithFeedback _sinkDevice; /// /// Gets the actual device instance for this destination. /// Lazily loads the device from the DeviceManager using the SinkKey. /// [JsonIgnore] - public IRoutingSink SinkDevice + public IRoutingSinkWithFeedback SinkDevice { - get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as IRoutingSink); } + get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as IRoutingSinkWithFeedback); } } /// diff --git a/src/PepperDash.Essentials.Core/Room/Interfaces.cs b/src/PepperDash.Essentials.Core/Room/Interfaces.cs index fd55755a..5d3c704c 100644 --- a/src/PepperDash.Essentials.Core/Room/Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Room/Interfaces.cs @@ -25,7 +25,7 @@ public interface IHasDefaultDisplay /// /// The default display for the room, used for presentation routing and other default routes /// - IRoutingSink DefaultDisplay { get; } + IRoutingSinkWithFeedback DefaultDisplay { get; } } /// diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index e2b30eee..19982f52 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -465,8 +465,8 @@ public static class Extensions IndexTieLines(); } - var sinks = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); - var sources = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); + var sinks = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingMidpoint)); + var sources = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingMidpoint)); foreach (var sink in sinks) { @@ -544,7 +544,7 @@ public static class Extensions /// The RouteDescriptor being populated as the route is discovered /// true if source is hit private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, - RoutingOutputPort outputPortToUse, List alreadyCheckedDevices, + RoutingOutputPort outputPortToUse, List alreadyCheckedDevices, eRoutingSignalType signalType, int cycle, RouteDescriptor routeTable, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) { cycle++; @@ -608,16 +608,16 @@ public static class Extensions // No direct tie? Run back out on the inputs' attached devices... // Only the ones that are routing devices - var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); + var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingMidpoint); //Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration if (alreadyCheckedDevices == null) - alreadyCheckedDevices = new List(); - alreadyCheckedDevices.Add(destination as IRoutingInputsOutputs); + alreadyCheckedDevices = new List(); + alreadyCheckedDevices.Add(destination as IRoutingMidpoint); foreach (var tieLine in midpointTieLines) { - var midpointDevice = tieLine.SourcePort.ParentDevice as IRoutingInputsOutputs; + var midpointDevice = tieLine.SourcePort.ParentDevice as IRoutingMidpoint; // Check if this previous device has already been walked if (alreadyCheckedDevices.Contains(midpointDevice)) @@ -659,12 +659,12 @@ public static class Extensions // we have a route on corresponding inputPort. *** Do the route *** - if (destination is IRoutingSink) + if (destination is IRoutingSinkWithFeedback) { // it's a sink device routeTable.Routes.Add(new RouteSwitchDescriptor(goodInputPort)); } - else if (destination is IRouting) + else if (destination is IRoutingMidpointWithFeedback) { routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort)); } diff --git a/src/PepperDash.Essentials.Core/Routing/IHasCurrentSourceInfoChange.cs b/src/PepperDash.Essentials.Core/Routing/IHasCurrentSourceInfoChange.cs deleted file mode 100644 index 107501b3..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IHasCurrentSourceInfoChange.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace PepperDash.Essentials.Core -{ - /// - /// Delegate for SourceInfoChangeHandler - /// - public delegate void SourceInfoChangeHandler(SourceListItem info, ChangeType type); - //******************************************************************************************* - // Interfaces - - /// - /// For rooms with a single presentation source, change event - /// - [Obsolete("Use ICurrentSources instead")] - public interface IHasCurrentSourceInfoChange - { - /// - /// The key for the current source info, used to look up the source in the SourceList - /// - string CurrentSourceInfoKey { get; set; } - - /// - /// The current source info for the room, used to look up the source in the SourceList - /// - SourceListItem CurrentSourceInfo { get; set; } - - /// - /// Event that is raised when the current source info changes. - /// This is used to notify the system of changes to the current source info. - /// The event handler receives the new source info and the type of change that occurred. - /// - event SourceInfoChangeHandler CurrentSourceChange; - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IMatrixRouting.cs b/src/PepperDash.Essentials.Core/Routing/IMatrixRouting.cs deleted file mode 100644 index d12e704e..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IMatrixRouting.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -namespace PepperDash.Essentials.Core.Routing -{ - /// - /// Defines the contract for IMatrixRouting - /// - public interface IMatrixRouting - { - /// - /// Gets the input slots - /// - Dictionary InputSlots { get; } - - /// - /// Gets the output slots - /// - Dictionary OutputSlots { get; } - - /// - /// Routes the specified input slot to the specified output slot for the specified signal type - /// - /// key of the input slot - /// key of the output slot - /// signal type - void Route(string inputSlotKey, string outputSlotKey, eRoutingSignalType type); - } -} diff --git a/src/PepperDash.Essentials.Core/Routing/IRmcRouting.cs b/src/PepperDash.Essentials.Core/Routing/IRmcRouting.cs deleted file mode 100644 index d2752f4a..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRmcRouting.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace PepperDash.Essentials.Core; - - -/// -/// Defines the contract for IRmcRouting -/// -public interface IRmcRouting : IRoutingNumeric -{ - /// - /// Feedback for the current Audio/Video source as a number - /// - IntFeedback AudioVideoSourceNumericFeedback { get; } -} diff --git a/src/PepperDash.Essentials.Core/Routing/IRmcRoutingWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRmcRoutingWithFeedback.cs deleted file mode 100644 index 660183c1..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRmcRoutingWithFeedback.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PepperDash.Essentials.Core; - -/// -/// Defines an IRmcRouting with a feedback event -/// -public interface IRmcRoutingWithFeedback : IRmcRouting -{ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRouting.cs b/src/PepperDash.Essentials.Core/Routing/IRouting.cs deleted file mode 100644 index e8ab124f..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRouting.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PepperDash.Essentials.Core; - -/// -/// Defines a midpoint device as have internal routing. Any devices in the middle of the -/// signal chain, that do switching, must implement this for routing to work otherwise -/// the routing algorithm will treat the IRoutingInputsOutputs device as a passthrough -/// device. -/// -public interface IRouting : IRoutingInputsOutputs - { - /// - /// Executes a switch on the device - /// - /// input selector - /// output selector - /// type of signal - void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType); -} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingFeedback.cs deleted file mode 100644 index a4e18e83..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingFeedback.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using PepperDash.Core; -namespace PepperDash.Essentials.Core; - -/// -/// Defines the contract for IRoutingFeedback -/// -public interface IRoutingFeedback : IKeyName -{ - /// - /// Event raised when a numeric switch changes - /// - event EventHandler NumericSwitchChange; - //void OnSwitchChange(RoutingNumericEventArgs e); -} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingInputSlot.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingInputSlot.cs deleted file mode 100644 index 0137d6c1..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingInputSlot.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PepperDash.Essentials.Core.Routing; - - -/// -/// Defines the contract for IRoutingInputSlot -/// -public interface IRoutingInputSlot : IRoutingSlot, IOnline, IVideoSync -{ - /// - /// Gets the Tx device key - /// - string TxDeviceKey { get; } -} - diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingInputsOutputs.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingInputsOutputs.cs deleted file mode 100644 index 9257b3d9..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingInputsOutputs.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace PepperDash.Essentials.Core; - -/// -/// Defines the contract for IRoutingInputsOutputs -/// -public interface IRoutingInputsOutputs : IRoutingInputs, IRoutingOutputs -{ -} - diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingMidpoint.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingMidpoint.cs new file mode 100644 index 00000000..e1a01521 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingMidpoint.cs @@ -0,0 +1,10 @@ +namespace PepperDash.Essentials.Core; + +/// +/// Defines a midpoint (passthrough) device that has both input and output routing ports +/// but does not perform active switching. For switching midpoints, see . +/// +public interface IRoutingMidpoint : IRoutingInputs, IRoutingOutputs +{ +} + diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingMidpointWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingMidpointWithFeedback.cs new file mode 100644 index 00000000..6c137c96 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingMidpointWithFeedback.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System; + +namespace PepperDash.Essentials.Core; + +/// +/// Delegate for RouteChangedEventHandler. +/// +/// The routing device where the change occurred. +/// A descriptor of the new route that was established. +public delegate void RouteChangedEventHandler(IRoutingMidpointWithFeedback midpoint, RouteSwitchDescriptor newRoute); + +/// +/// Defines a midpoint device that performs active switching and provides feedback about its current routes. +/// Combines the capabilities of the former IRouting, IRoutingWithFeedback, and IRoutingWithClear interfaces. +/// +public interface IRoutingMidpointWithFeedback : IRoutingMidpoint +{ + /// + /// Executes a switch on the device. + /// + /// Input selector. + /// Output selector. + /// Type of signal. + void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType); + + /// + /// Clears a route to an output. + /// + /// Output to clear. + /// Signal type to clear. + void ClearRoute(object outputSelector, eRoutingSignalType signalType); + + /// + /// Gets a list describing the currently active routes on this device. + /// + List CurrentRoutes { get; } + + /// + /// Event triggered when a route changes on this device. + /// + event RouteChangedEventHandler RouteChanged; +} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingNumeric.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingNumeric.cs deleted file mode 100644 index ba334afa..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingNumeric.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace PepperDash.Essentials.Core; - - -/// -/// Defines the contract for IRoutingNumeric -/// -public interface IRoutingNumeric : IRouting -{ - /// - /// Executes a numeric switch on the device - /// - /// input selector - /// output selector - /// type of signal - void ExecuteNumericSwitch(ushort input, ushort output, eRoutingSignalType type); -} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingNumericWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingNumericWithFeedback.cs deleted file mode 100644 index a59e9699..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingNumericWithFeedback.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PepperDash.Essentials.Core; - -/// -/// Defines an IRoutingNumeric with a feedback event -/// -public interface IRoutingNumericWithFeedback : IRoutingNumeric, IRoutingFeedback -{ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingOutputSlot.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingOutputSlot.cs deleted file mode 100644 index ded827e7..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingOutputSlot.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace PepperDash.Essentials.Core.Routing; - - -/// -/// Defines the contract for IRoutingOutputSlot -/// -public interface IRoutingOutputSlot : IRoutingSlot -{ - /// - /// Event raised when output slot changes - /// - event EventHandler OutputSlotChanged; - - /// - /// Gets the Rx device key - /// - string RxDeviceKey { get; } - - /// - /// Gets the current routes - /// - Dictionary CurrentRoutes { get; } -} - diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs deleted file mode 100644 index 9426474a..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSink.cs +++ /dev/null @@ -1,24 +0,0 @@ -using PepperDash.Core; -using PepperDash.Essentials.Core.Routing; - -namespace PepperDash.Essentials.Core; - -/// -/// Defines the contract for IRoutingSink -/// -public interface IRoutingSink : IRoutingInputs, IKeyName, ICurrentSources -{ -} - -/// -/// For fixed-source endpoint devices with an input port -/// -public interface IRoutingSinkWithInputPort : IRoutingSink -{ - /// - /// Gets the current input port for this routing sink. - /// - RoutingInputPort CurrentInputPort { get; } -} - - diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs index d4f9c484..b6f578c7 100644 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithFeedback.cs @@ -1,12 +1,37 @@ -namespace PepperDash.Essentials.Core; - +using PepperDash.Core; +using PepperDash.Essentials.Core.Routing; +namespace PepperDash.Essentials.Core; /// -/// Defines the contract for IRoutingSinkWithFeedback +/// Delegate for InputChangedEventHandler. /// -public interface IRoutingSinkWithFeedback : IRoutingSink +/// The sink device that changed input. +/// The new input port selected on the sink device. +public delegate void InputChangedEventHandler(IRoutingSinkWithFeedback destination, RoutingInputPort currentPort); + +/// +/// Defines a routing sink (endpoint) device that can switch inputs and provides feedback. +/// Consolidates the former IRoutingSink, IRoutingSinkWithInputPort, IRoutingSinkWithSwitching, +/// IRoutingSinkWithSwitchingWithInputPort, and IRoutingSinkWithFeedback interfaces. +/// +public interface IRoutingSinkWithFeedback : IRoutingInputs, IKeyName, ICurrentSources { + /// + /// Executes a switch on the device. + /// + /// Input selector. + void ExecuteSwitch(object inputSelector); + + /// + /// Gets the current input port for this routing sink. + /// + RoutingInputPort CurrentInputPort { get; } + + /// + /// Event raised when the input changes. + /// + event InputChangedEventHandler InputChanged; } diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithSwitching.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithSwitching.cs deleted file mode 100644 index 88e1f72b..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithSwitching.cs +++ /dev/null @@ -1,30 +0,0 @@ - -namespace PepperDash.Essentials.Core; - -/// -/// Delegate for InputChangedEventHandler -/// -public delegate void InputChangedEventHandler(IRoutingSinkWithSwitching destination, RoutingInputPort currentPort); - -/// -/// Defines the contract for IRoutingSinkWithSwitching -/// -public interface IRoutingSinkWithSwitching : IRoutingSink -{ - /// - /// Executes a switch on the device - /// - /// input selector - void ExecuteSwitch(object inputSelector); -} - -/// -/// Defines the contract for IRoutingSinkWithSwitchingWithInputPort -/// -public interface IRoutingSinkWithSwitchingWithInputPort : IRoutingSinkWithSwitching, IRoutingSinkWithInputPort -{ - /// - /// Event raised when the input changes - /// - event InputChangedEventHandler InputChanged; -} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSlot.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSlot.cs deleted file mode 100644 index deb5cabe..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingSlot.cs +++ /dev/null @@ -1,20 +0,0 @@ -using PepperDash.Core; - -namespace PepperDash.Essentials.Core.Routing -{ - /// - /// Defines the contract for IRoutingSlot - /// - public interface IRoutingSlot:IKeyName - { - /// - /// Gets the slot number - /// - int SlotNumber { get; } - - /// - /// Gets the supported signal types - /// - eRoutingSignalType SupportedSignalTypes { get; } - } -} diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingWithClear.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingWithClear.cs deleted file mode 100644 index dc027db9..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingWithClear.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PepperDash.Essentials.Core; - -/// -/// Defines a routing device () that supports explicitly clearing a route on an output. -/// -public interface IRoutingWithClear : IRouting -{ - /// - /// Clears a route to an output, however a device needs to do that - /// - /// Output to clear - /// signal type to clear - void ClearRoute(object outputSelector, eRoutingSignalType signalType); -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingWithFeedback.cs deleted file mode 100644 index 6578ee4f..00000000 --- a/src/PepperDash.Essentials.Core/Routing/IRoutingWithFeedback.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using System; - -namespace PepperDash.Essentials.Core; - -/// -/// Gets a list describing the currently active routes on this device. -/// -/// The routing device where the change occurred. -/// A descriptor of the new route that was established. -/// -/// Delegate for RouteChangedEventHandler -/// -public delegate void RouteChangedEventHandler(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute); -/// -/// Defines a routing device () that provides feedback about its current routes. -/// -public interface IRoutingWithFeedback : IRouting -{ - /// - /// Gets a list describing the currently active routes on this device. - /// - List CurrentRoutes { get; } - - /// - /// Event triggered when a route changes on this device. - /// - event RouteChangedEventHandler RouteChanged; -} diff --git a/src/PepperDash.Essentials.Core/Routing/ITxRouting.cs b/src/PepperDash.Essentials.Core/Routing/ITxRouting.cs deleted file mode 100644 index 67901117..00000000 --- a/src/PepperDash.Essentials.Core/Routing/ITxRouting.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace PepperDash.Essentials.Core; - -/// -/// Represents a routing device (typically a transmitter or source) that provides numeric feedback for its current route. -/// Extends . -/// -public interface ITxRouting : IRoutingNumeric -{ - /// - /// Feedback indicating the currently routed video source by its numeric identifier. - /// - IntFeedback VideoSourceNumericFeedback { get; } - /// - /// Feedback indicating the currently routed audio source by its numeric identifier. - /// - IntFeedback AudioSourceNumericFeedback { get; } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/ITxRoutingWithFeedback.cs b/src/PepperDash.Essentials.Core/Routing/ITxRoutingWithFeedback.cs deleted file mode 100644 index 0ba49439..00000000 --- a/src/PepperDash.Essentials.Core/Routing/ITxRoutingWithFeedback.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PepperDash.Essentials.Core; - -/// -/// Defines an IRmcRouting with a feedback event -/// -public interface ITxRoutingWithFeedback : ITxRouting -{ -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs index fa68a42d..631aff62 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs @@ -74,13 +74,13 @@ public class RouteDescriptor { Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString()); - if (route.SwitchingDevice is IRoutingSinkWithSwitching sink) + if (route.SwitchingDevice is IRoutingSinkWithFeedback sink) { sink.ExecuteSwitch(route.InputPort.Selector); continue; } - if (route.SwitchingDevice is IRouting switchingDevice) + if (route.SwitchingDevice is IRoutingMidpointWithFeedback switchingDevice) { switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); @@ -97,9 +97,9 @@ public class RouteDescriptor /// If true, attempts to clear the route on the switching devices (e.g., set input to null/0). public void ReleaseRoutes(bool clearRoute = false) { - foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting)) + foreach (var route in Routes.Where(r => r.SwitchingDevice is IRoutingMidpointWithFeedback)) { - if (route.SwitchingDevice is IRouting switchingDevice) + if (route.SwitchingDevice is IRoutingMidpointWithFeedback switchingDevice) { if (clearRoute) { diff --git a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs index 227e6d5b..38d56b93 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs @@ -45,7 +45,7 @@ /// public override string ToString() { - if (SwitchingDevice is IRouting) + if (SwitchingDevice is IRoutingMidpointWithFeedback) return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}"; else return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}"; diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 73401600..15855cf4 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -49,8 +49,8 @@ namespace PepperDash.Essentials.Core.Routing { midpointToSinksMap = new Dictionary>(); - var sinks = DeviceManager.AllDevices.OfType(); - var midpoints = DeviceManager.AllDevices.OfType(); + var sinks = DeviceManager.AllDevices.OfType(); + var midpoints = DeviceManager.AllDevices.OfType(); foreach (var sink in sinks) { @@ -80,7 +80,7 @@ namespace PepperDash.Essentials.Core.Routing /// /// Gets all upstream midpoint device keys for a given sink /// - private HashSet GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink) + private HashSet GetUpstreamMidpoints(IRoutingSinkWithFeedback sink) { var result = new HashSet(); var visited = new HashSet(); @@ -109,7 +109,7 @@ namespace PepperDash.Essentials.Core.Routing visited.Add(tieLine.SourcePort.ParentDevice.Key); - if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint) + if (tieLine.SourcePort.ParentDevice is IRoutingMidpointWithFeedback midpoint) { midpoints.Add(midpoint.Key); @@ -131,11 +131,11 @@ namespace PepperDash.Essentials.Core.Routing } /// - /// Subscribes to the RouteChanged event on all devices implementing . + /// Subscribes to the RouteChanged event on all devices implementing . /// private void SubscribeForMidpointFeedback() { - var midpointDevices = DeviceManager.AllDevices.OfType(); + var midpointDevices = DeviceManager.AllDevices.OfType(); foreach (var device in midpointDevices) { @@ -144,12 +144,12 @@ namespace PepperDash.Essentials.Core.Routing } /// - /// Subscribes to the InputChanged event on all devices implementing . + /// Subscribes to the InputChanged event on all devices implementing . /// private void SubscribeForSinkFeedback() { var sinkDevices = - DeviceManager.AllDevices.OfType(); + DeviceManager.AllDevices.OfType(); foreach (var device in sinkDevices) { @@ -164,7 +164,7 @@ namespace PepperDash.Essentials.Core.Routing /// The midpoint device that reported a route change. /// The descriptor of the new route. private void HandleMidpointUpdate( - IRoutingWithFeedback midpoint, + IRoutingMidpointWithFeedback midpoint, RouteSwitchDescriptor newRoute ) { @@ -183,7 +183,7 @@ namespace PepperDash.Essentials.Core.Routing foreach (var sinkKey in affectedSinkKeys) { - if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithSwitchingWithInputPort sink) + if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithFeedback sink) { UpdateDestination(sink, sink.CurrentInputPort); } @@ -218,7 +218,7 @@ namespace PepperDash.Essentials.Core.Routing /// The sink device that reported an input change. /// The new input port selected on the sink device. private void HandleSinkUpdate( - IRoutingSinkWithSwitching sender, + IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort ) { @@ -246,7 +246,7 @@ namespace PepperDash.Essentials.Core.Routing /// The destination sink device to update. /// The currently selected input port on the destination device. private void UpdateDestination( - IRoutingSinkWithSwitching destination, + IRoutingSinkWithFeedback destination, RoutingInputPort inputPort ) { @@ -298,7 +298,7 @@ namespace PepperDash.Essentials.Core.Routing /// Called after debounce delay. /// private void UpdateDestinationImmediate( - IRoutingSinkWithSwitching destination, + IRoutingSinkWithFeedback destination, RoutingInputPort inputPort ) { @@ -491,7 +491,7 @@ namespace PepperDash.Essentials.Core.Routing // Get all potential sources (devices that only have outputs, not inputs+outputs) var sources = DeviceManager.AllDevices .OfType() - .Where(s => !(s is IRoutingInputsOutputs)); + .Where(s => !(s is IRoutingMidpoint)); // Try each signal type that this TieLine supports var signalTypes = new[] diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs index f924c951..d532e7a2 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs @@ -62,8 +62,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers })]; } - // Check if device implements IRoutingInputsOutputs - if (device is IRoutingInputsOutputs) + // Check if device implements IRoutingMidpoint + if (device is IRoutingMidpoint) { deviceInfo.HasInputsAndOutputs = true; } diff --git a/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs b/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs index 252e4dc4..24df64ae 100644 --- a/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs +++ b/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs @@ -12,11 +12,16 @@ namespace PepperDash.Essentials.Devices.Common; /// /// Represents and audio endpoint /// -public class GenericAudioOut : EssentialsDevice, IRoutingSink +public class GenericAudioOut : EssentialsDevice, IRoutingSinkWithFeedback { /// public RoutingInputPort CurrentInputPort => AnyAudioIn; + /// + public event InputChangedEventHandler InputChanged; + + /// + public void ExecuteSwitch(object inputSelector) { } /// public Dictionary CurrentSources { get; private set; } diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs index 5a018a84..9c8137d7 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/BasicIrDisplay.cs @@ -229,7 +229,7 @@ public class BasicIrDisplay : DisplayBase, IBasicVolumeControls, IBridgeAdvanced t.Start(); } - #region IRoutingSink Members + #region IRoutingSinkWithFeedback Members /// /// Typically called by the discovery routing algorithm. diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs index a9d9a261..f176c24c 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs @@ -15,7 +15,7 @@ namespace PepperDash.Essentials.Devices.Common.Displays; /// /// Represents a mock display device for testing and simulation purposes. /// -public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs, IRoutingSinkWithSwitchingWithInputPort, IHasPowerControlWithFeedback +public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs, IHasPowerControlWithFeedback { /// public ISelectableItems Inputs { get; private set; } diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/TwoWayDisplayBase.cs b/src/PepperDash.Essentials.Devices.Common/Displays/TwoWayDisplayBase.cs index 226d4bbe..92dc2007 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/TwoWayDisplayBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/TwoWayDisplayBase.cs @@ -7,7 +7,7 @@ namespace PepperDash.Essentials.Devices.Common.Displays /// Abstract base class for two-way display devices that provide feedback capabilities. /// Extends DisplayBase with routing feedback and power control feedback functionality. /// - public abstract class TwoWayDisplayBase : DisplayBase, IRoutingFeedback, IHasPowerControlWithFeedback + public abstract class TwoWayDisplayBase : DisplayBase, IHasPowerControlWithFeedback { /// /// Gets feedback for the current input selection on the display. diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index 7e449564..be3590ad 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -12,7 +12,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic; /// /// Represents a GenericSink /// -public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort, ICurrentSources +public class GenericSink : EssentialsDevice, IRoutingSinkWithFeedback { /// public Dictionary CurrentSources { get; private set; } @@ -104,43 +104,13 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputP /// public RoutingPortCollection InputPorts { get; private set; } - /// - /// Gets or sets the CurrentSourceInfoKey - /// - public string CurrentSourceInfoKey { get; set; } - private SourceListItem _currentSource; - /// - /// Gets or sets the CurrentSourceInfo - /// - public SourceListItem CurrentSourceInfo - { - get => _currentSource; - set - { - if (value == _currentSource) - { - return; - } - - CurrentSourceChange?.Invoke(_currentSource, ChangeType.WillChange); - - _currentSource = value; - - CurrentSourceChange?.Invoke(_currentSource, ChangeType.DidChange); - } - } /// /// Gets the current input port /// public RoutingInputPort CurrentInputPort => InputPorts[0]; - /// - /// Event fired when the current source changes - /// - public event SourceInfoChangeHandler CurrentSourceChange; - /// public event InputChangedEventHandler InputChanged; diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs index 54a9d989..f2ab5254 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs @@ -15,7 +15,7 @@ namespace PepperDash.Essentials.Devices.Common.SoftCodec; /// /// Class representing a BlueJeans soft codec running on an in-room PC. /// -public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink +public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSinkWithFeedback { /// @@ -29,6 +29,12 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink /// public RoutingInputPort CurrentInputPort => AnyVideoIn; + /// + public event InputChangedEventHandler InputChanged; + + /// + public void ExecuteSwitch(object inputSelector) { } + /// public Dictionary CurrentSources { get; private set; } @@ -170,16 +176,7 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink } // store the name and UI info for routes - if (item.SourceKey == "none") - { - CurrentSourceInfoKey = routeKey; - CurrentSourceInfo = null; - } - else if (item.SourceKey != null) - { - CurrentSourceInfoKey = routeKey; - CurrentSourceInfo = item; - } + // CurrentSourceInfo tracking removed in v3 interface consolidation // report back when done if (successCallback != null) @@ -196,9 +193,9 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink /// bool DoRoute(SourceRouteListItem route) { - IRoutingSink dest = null; + IRoutingSinkWithFeedback dest = null; - dest = DeviceManager.GetDeviceForKey(route.DestinationKey) as IRoutingSink; + dest = DeviceManager.GetDeviceForKey(route.DestinationKey) as IRoutingSinkWithFeedback; if (dest == null) { @@ -227,42 +224,5 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSink - #region IHasCurrentSourceInfoChange Members - /// - public string CurrentSourceInfoKey { get; set; } - - /// - /// The SourceListItem last run - containing names and icons - /// - public SourceListItem CurrentSourceInfo - { - get { return _CurrentSourceInfo; } - set - { - if (value == _CurrentSourceInfo) return; - - var handler = CurrentSourceChange; - // remove from in-use tracker, if so equipped - if (_CurrentSourceInfo != null && _CurrentSourceInfo.SourceDevice is IInUseTracking) - (_CurrentSourceInfo.SourceDevice as IInUseTracking).InUseTracker.RemoveUser(this, "control"); - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.WillChange); - - _CurrentSourceInfo = value; - - // add to in-use tracking - if (_CurrentSourceInfo != null && _CurrentSourceInfo.SourceDevice is IInUseTracking) - (_CurrentSourceInfo.SourceDevice as IInUseTracking).InUseTracker.AddUser(this, "control"); - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.DidChange); - } - } - SourceListItem _CurrentSourceInfo; - - /// - public event SourceInfoChangeHandler CurrentSourceChange; - - #endregion } diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs index 9cc4c1d8..3e40f6b6 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs @@ -12,7 +12,7 @@ namespace PepperDash.Essentials.Devices.Common.SoftCodec; /// /// Represents a GenericSoftCodec /// -public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWithSwitchingWithInputPort +public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWithFeedback { private RoutingInputPort _currentInputPort; @@ -141,46 +141,11 @@ public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWi /// public RoutingPortCollection InputPorts { get; private set; } + /// /// /// Gets or sets the OutputPorts /// public RoutingPortCollection OutputPorts { get; private set; } - /// - /// Gets or sets the CurrentSourceInfoKey - /// - public string CurrentSourceInfoKey { get; set; } - - /// - /// Gets or sets the CurrentSourceInfo - /// - public SourceListItem CurrentSourceInfo - { - get - { - return _CurrentSourceInfo; - } - set - { - if (value == _CurrentSourceInfo) return; - - var handler = CurrentSourceChange; - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.WillChange); - - _CurrentSourceInfo = value; - - if (handler != null) - handler(_CurrentSourceInfo, ChangeType.DidChange); - } - } - - SourceListItem _CurrentSourceInfo; - - /// - /// Event fired when the current source changes - /// - public event SourceInfoChangeHandler CurrentSourceChange; /// /// Event fired when the input changes diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index 59f6097b..de6900af 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -27,7 +27,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec; /// Base class for video codecs. Contains common properties, methods, and feedback for video codecs. /// Also contains the logic to link commonly implemented interfaces to the API bridge. /// -public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs, +public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingMidpoint, IUsageTracking, ICodecCallControls, IHasContentSharing, ICodecAudio, IVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode, IHasReady { private const int XSigEncoding = 28591; @@ -302,7 +302,7 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu #endregion - #region IRoutingInputsOutputs Members + #region IRoutingMidpoint Members /// /// Gets or sets the InputPorts diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithFeedbackMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithFeedbackMessenger.cs new file mode 100644 index 00000000..d2883903 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithFeedbackMessenger.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.AppServer; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; +using System.Linq; +using DisplayBase = PepperDash.Essentials.Devices.Common.Displays.DisplayBase; + +namespace PepperDash.Essentials.Room.MobileControl +{ + /// + /// Represents a DisplayBaseMessenger + /// + public class IRoutingSinkWithFeedbackMessenger : MessengerBase + { + private readonly IRoutingSinkWithFeedback display; + + /// + /// Create an instance of the class. + /// + /// + /// + /// + public IRoutingSinkWithFeedbackMessenger(string key, string messagePath, IRoutingSinkWithFeedback device) : base(key, messagePath, device) + { + display = device; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + /*AddAction("/powerOn", (id, content) => display.PowerOn()); + AddAction("/powerOff", (id, content) => display.PowerOff()); + AddAction("/powerToggle", (id, content) => display.PowerToggle());*/ + + AddAction("/inputSelect", (id, content) => + { + var s = content.ToObject>(); + + var inputPort = display.InputPorts.FirstOrDefault(i => i.Key == s.Value); + + if (inputPort == null) + { + this.LogWarning("No input named {inputName} found for {deviceKey}", s, display.Key); + return; + } + + display.ExecuteSwitch(inputPort.Selector); + }); + + AddAction("/inputs", (id, content) => + { + var inputsList = display.InputPorts.Select(p => p.Key).ToList(); + + var messageObject = new MobileControlMessage + { + Type = MessagePath + "/inputs", + Content = JToken.FromObject(new + { + inputKeys = inputsList, + }) + }; + + AppServerController.SendMessageObject(messageObject); + }); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs index 1426dc25..1298608b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs @@ -12,17 +12,17 @@ namespace PepperDash.Essentials.Room.MobileControl /// /// Represents a DisplayBaseMessenger /// - public class IRoutingSinkWithSwitchingMessenger : MessengerBase + public class IRoutingSinkWithFeedbackMessenger : MessengerBase { - private readonly IRoutingSinkWithSwitching display; + private readonly IRoutingSinkWithFeedback display; /// - /// Create an instance of the class. + /// Create an instance of the class. /// /// /// /// - public IRoutingSinkWithSwitchingMessenger(string key, string messagePath, IRoutingSinkWithSwitching device) : base(key, messagePath, device) + public IRoutingSinkWithFeedbackMessenger(string key, string messagePath, IRoutingSinkWithFeedback device) : base(key, messagePath, device) { display = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs deleted file mode 100644 index 2a8dd5bc..00000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using PepperDash.Core; -using PepperDash.Essentials.Core; - -namespace PepperDash.Essentials.AppServer.Messengers -{ - /// - /// Represents a IHasCurrentSourceInfoMessenger - /// - public class IHasCurrentSourceInfoMessenger : MessengerBase - { - private readonly IHasCurrentSourceInfoChange sourceDevice; - - public IHasCurrentSourceInfoMessenger(string key, string messagePath, IHasCurrentSourceInfoChange device) : base(key, messagePath, device as IKeyName) - { - sourceDevice = device; - } - - protected override void RegisterActions() - { - base.RegisterActions(); - - AddAction("/fullStatus", (id, content) => SendFullStatus(id)); - - AddAction("/currentSourceInfoStatus", (id, content) => SendFullStatus(id)); - - sourceDevice.CurrentSourceChange += (sender, e) => - { - switch (e) - { - case ChangeType.DidChange: - { - PostStatusMessage(JToken.FromObject(new - { - currentSourceKey = string.IsNullOrEmpty(sourceDevice.CurrentSourceInfoKey) ? string.Empty : sourceDevice.CurrentSourceInfoKey, - currentSource = sourceDevice.CurrentSourceInfo - })); - break; - } - } - }; - } - - private void SendFullStatus(string id = null) - { - var message = new CurrentSourceStateMessage - { - CurrentSourceKey = sourceDevice.CurrentSourceInfoKey, - CurrentSource = sourceDevice.CurrentSourceInfo - }; - - PostStatusMessage(message, id); - } - } - - /// - /// Represents a CurrentSourceStateMessage - /// - public class CurrentSourceStateMessage : DeviceStateMessageBase - { - - /// - /// Gets or sets the CurrentSourceKey - /// - [JsonProperty("currentSourceKey", NullValueHandling = NullValueHandling.Ignore)] - public string CurrentSourceKey { get; set; } - - - /// - /// Gets or sets the CurrentSource - /// - [JsonProperty("currentSource")] - public SourceListItem CurrentSource { get; set; } - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs deleted file mode 100644 index 3ea457d2..00000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using PepperDash.Core; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.Routing; -using Serilog.Events; - -namespace PepperDash.Essentials.AppServer.Messengers -{ - /// - /// Messenger for devices that implment IMatrixRouting - /// - public class IMatrixRoutingMessenger : MessengerBase - { - private readonly IMatrixRouting matrixDevice; - - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public IMatrixRoutingMessenger(string key, string messagePath, IMatrixRouting device) : base(key, messagePath, device as IKeyName) - { - matrixDevice = device; - } - - /// - protected override void RegisterActions() - { - base.RegisterActions(); - - AddAction("/fullStatus", (id, content) => SendFullStatus(id)); - - AddAction("/matrixStatus", (id, content) => SendFullStatus(id)); - - AddAction("/route", (id, content) => - { - var request = content.ToObject(); - - matrixDevice.Route(request.InputKey, request.OutputKey, request.RouteType); - }); - - foreach (var output in matrixDevice.OutputSlots) - { - var key = output.Key; - var outputSlot = output.Value; - - outputSlot.OutputSlotChanged += (sender, args) => - { - PostStatusMessage(JToken.FromObject(new - { - outputs = matrixDevice.OutputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingOutput(kvp.Value)) - })); - }; - } - - foreach (var input in matrixDevice.InputSlots) - { - var key = input.Key; - var inputSlot = input.Value; - - inputSlot.VideoSyncChanged += (sender, args) => - { - PostStatusMessage(JToken.FromObject(new - { - inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value)) - })); - }; - - inputSlot.IsOnline.OutputChange += (sender, args) => - { - PostStatusMessage(JToken.FromObject(new - { - inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value)) - })); - }; - } - } - - private void SendFullStatus(string id = null) - { - try - { - Debug.LogMessage(LogEventLevel.Verbose, "InputCount: {inputCount}, OutputCount: {outputCount}", this, matrixDevice.InputSlots.Count, matrixDevice.OutputSlots.Count); - var message = new MatrixStateMessage - { - Outputs = matrixDevice.OutputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingOutput(kvp.Value)), - Inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value)), - }; - - - PostStatusMessage(message, id); - } - catch (Exception e) - { - Debug.LogMessage(e, "Exception Getting full status: {@exception}", this, e); - } - } - } - - /// - /// Represents a MatrixStateMessage - /// - public class MatrixStateMessage : DeviceStateMessageBase - { - /// - /// Gets or sets the Outputs - /// - [JsonProperty("outputs")] - public Dictionary Outputs; - - /// - /// Gets or sets the Inputs - /// - [JsonProperty("inputs")] - public Dictionary Inputs; - } - - /// - /// Represents a RoutingInput - /// - public class RoutingInput : IKeyName - { - private IRoutingInputSlot _input; - - /// - /// Gets the TxDeviceKey of the input slot - /// - [JsonProperty("txDeviceKey", NullValueHandling = NullValueHandling.Ignore)] - public string TxDeviceKey => _input?.TxDeviceKey; - - /// - /// Gets the SlotNumber of the input slot - /// - /// - [JsonProperty("slotNumber", NullValueHandling = NullValueHandling.Ignore)] - public int? SlotNumber => _input?.SlotNumber; - - /// - /// Gets the SupportedSignalTypes of the input slot - /// - [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - [JsonProperty("supportedSignalTypes", NullValueHandling = NullValueHandling.Ignore)] - public eRoutingSignalType? SupportedSignalTypes => _input?.SupportedSignalTypes; - - /// - /// Gets the Name of the input slot - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name => _input?.Name; - - /// - /// Gets the IsOnline of the input slot - /// - /// - [JsonProperty("isOnline", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsOnline => _input?.IsOnline.BoolValue; - - - /// - /// Gets the VideoSyncDetected of the input slot - /// - [JsonProperty("videoSyncDetected", NullValueHandling = NullValueHandling.Ignore)] - public bool? VideoSyncDetected => _input?.VideoSyncDetected; - - /// - /// Gets the Key of the input slot - /// - [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] - public string Key => _input?.Key; - - /// - /// Initializes a new instance of the class. - /// - /// - public RoutingInput(IRoutingInputSlot input) - { - _input = input; - } - } - - /// - /// Represents a RoutingOutput - /// - public class RoutingOutput : IKeyName - { - private IRoutingOutputSlot _output; - - /// - /// Initializes a new instance of the class. - /// - /// - public RoutingOutput(IRoutingOutputSlot output) - { - _output = output; - } - - /// - /// Gets the RxDeviceKey of the output slot - /// - [JsonProperty("rxDeviceKey")] - public string RxDeviceKey => _output.RxDeviceKey; - - /// - /// Gets the CurrentRoutes of the output slot - /// - [JsonProperty("currentRoutes")] - public Dictionary CurrentRoutes => _output.CurrentRoutes.ToDictionary(kvp => kvp.Key.ToString(), kvp => new RoutingInput(kvp.Value)); - - /// - /// Gets the SlotNumber of the output slot - /// - [JsonProperty("slotNumber")] - public int SlotNumber => _output.SlotNumber; - - /// - /// Gets the SupportedSignalTypes of the output slot - /// - [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - [JsonProperty("supportedSignalTypes")] - public eRoutingSignalType SupportedSignalTypes => _output.SupportedSignalTypes; - - - /// - /// Gets the Name of the output slot - /// - [JsonProperty("name")] - public string Name => _output.Name; - - - /// - /// Gets the Key of the output slot - /// - [JsonProperty("key")] - public string Key => _output.Key; - } - - /// - /// Represents a MatrixRouteRequest - /// - public class MatrixRouteRequest - { - /// - /// Gets or sets the OutputKey - /// - [JsonProperty("outputKey")] - public string OutputKey { get; set; } - - /// - /// Gets or sets the InputKey - /// - [JsonProperty("inputKey")] - public string InputKey { get; set; } - - /// - /// Gets or sets the RouteType - /// - [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - [JsonProperty("routeType")] - public eRoutingSignalType RouteType { get; set; } - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoutingMidpointWithFeedbackMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoutingMidpointWithFeedbackMessenger.cs new file mode 100644 index 00000000..1f5c7789 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRoutingMidpointWithFeedbackMessenger.cs @@ -0,0 +1,95 @@ +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices that implement IRoutingMidpointWithFeedback + /// + public class IRoutingMidpointWithFeedbackMessenger : MessengerBase + { + private readonly IRoutingMidpointWithFeedback _device; + + public IRoutingMidpointWithFeedbackMessenger(string key, string messagePath, IRoutingMidpointWithFeedback device) + : base(key, messagePath, device as IKeyName) + { + _device = device; + } + + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/route", (id, content) => + { + var request = content.ToObject(); + _device.ExecuteSwitch(request.InputSelector, request.OutputSelector, request.SignalType); + }); + + AddAction("/clearRoute", (id, content) => + { + var request = content.ToObject(); + _device.ClearRoute(request.OutputSelector, request.SignalType); + }); + + _device.RouteChanged += OnRouteChanged; + } + + private void OnRouteChanged(IRoutingMidpointWithFeedback midpoint, RouteSwitchDescriptor newRoute) + { + PostStatusMessage(JToken.FromObject(new + { + currentRoutes = _device.CurrentRoutes.Select(r => new + { + inputPort = r.InputPort?.Key, + outputPort = r.OutputPort?.Key + }) + })); + } + + private void SendFullStatus(string id = null) + { + var message = JToken.FromObject(new + { + inputPorts = _device.InputPorts.Select(p => new { key = p.Key }), + outputPorts = _device.OutputPorts.Select(p => new { key = p.Key }), + currentRoutes = _device.CurrentRoutes.Select(r => new + { + inputPort = r.InputPort?.Key, + outputPort = r.OutputPort?.Key + }) + }); + + PostStatusMessage(message, id); + } + } + + public class MidpointRouteRequest + { + [JsonProperty("inputSelector")] + public object InputSelector { get; set; } + + [JsonProperty("outputSelector")] + public object OutputSelector { get; set; } + + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + [JsonProperty("signalType")] + public eRoutingSignalType SignalType { get; set; } + } + + public class MidpointClearRouteRequest + { + [JsonProperty("outputSelector")] + public object OutputSelector { get; set; } + + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + [JsonProperty("signalType")] + public eRoutingSignalType SignalType { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index 6caae7b2..e3454031 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -116,9 +116,9 @@ namespace PepperDash.Essentials // ── Displays ───────────────────────────────────────────────────────────── new MessengerFactoryEntry( - typeof(IRoutingSinkWithSwitching), - (d, mp, ck) => new IRoutingSinkWithSwitchingMessenger( - $"{d.Key}-displayBase-{ck}", mp, (IRoutingSinkWithSwitching)d) + typeof(IRoutingSinkWithFeedback), + (d, mp, ck) => new IRoutingSinkWithFeedbackMessenger( + $"{d.Key}-displayBase-{ck}", mp, (IRoutingSinkWithFeedback)d) ), new MessengerFactoryEntry( typeof(IDisplayCurrentInput), @@ -337,9 +337,9 @@ namespace PepperDash.Essentials // ── Matrix routing ──────────────────────────────────────────────────────── // Preserving original key format (no controller key suffix) new MessengerFactoryEntry( - typeof(IMatrixRouting), - (d, mp, _) => new IMatrixRoutingMessenger( - $"{d.Key}-matrixRouting", mp, (IMatrixRouting)d) + typeof(IRoutingMidpointWithFeedback), + (d, mp, _) => new IRoutingMidpointWithFeedbackMessenger( + $"{d.Key}-matrixRouting", mp, (IRoutingMidpointWithFeedback)d) ), // ── Environmental sensors ───────────────────────────────────────────────── diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs index b6aca416..22143a51 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -140,9 +140,6 @@ namespace PepperDash.Essentials.RoomBridges if (Room is IRunDefaultPresentRoute defaultRoom) AddAction("/defaultsource", (id, content) => defaultRoom.RunDefaultPresentRoute()); - if (Room is IHasCurrentSourceInfoChange sscRoom) - sscRoom.CurrentSourceChange += Room_CurrentSingleSourceChange; - if (Room is IPrivacy privacyRoom) { @@ -353,9 +350,9 @@ namespace PepperDash.Essentials.RoomBridges string shareText; bool isSharing; - if (Room is IHasCurrentSourceInfoChange srcInfoRoom && Room is IHasVideoCodec vcRoom && vcRoom.VideoCodec.SharingContentIsOnFeedback.BoolValue && srcInfoRoom.CurrentSourceInfo != null) + if (Room is IHasVideoCodec vcRoom && vcRoom.VideoCodec.SharingContentIsOnFeedback.BoolValue) { - shareText = srcInfoRoom.CurrentSourceInfo.PreferredName; + shareText = "Sharing"; isSharing = true; } else @@ -470,18 +467,7 @@ namespace PepperDash.Essentials.RoomBridges } - private void Room_CurrentSingleSourceChange(SourceListItem info, ChangeType type) - { - /* Example message - * { -   "type":"/room/status", -   "content": { -     "selectedSourceKey": "off", -   } - } - */ - } /// /// Sends the full status of the room to the server @@ -512,7 +498,7 @@ namespace PepperDash.Essentials.RoomBridges { this.LogVerbose("GetFullStatus"); - var sourceKey = room is IHasCurrentSourceInfoChange ? (room as IHasCurrentSourceInfoChange).CurrentSourceInfoKey : null; + var sourceKey = (string)null; var volumes = new Dictionary(); if (room is IHasCurrentVolumeControls rmVc) From ec7db1b510501a086ac85fd80eb41d867b6cdf86 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 14 May 2026 15:38:59 -0600 Subject: [PATCH 067/161] feat: Remove IRoutingSinkWithFeedbackMessenger class and its associated actions --- .../IRoutingSinkWithSwitchingMessenger.cs | 71 ------------------- 1 file changed, 71 deletions(-) delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs deleted file mode 100644 index 1298608b..00000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IRoutingSinkWithSwitchingMessenger.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Newtonsoft.Json.Linq; -using PepperDash.Core; -using PepperDash.Core.Logging; -using PepperDash.Essentials.AppServer; -using PepperDash.Essentials.AppServer.Messengers; -using PepperDash.Essentials.Core; -using System.Linq; -using DisplayBase = PepperDash.Essentials.Devices.Common.Displays.DisplayBase; - -namespace PepperDash.Essentials.Room.MobileControl -{ - /// - /// Represents a DisplayBaseMessenger - /// - public class IRoutingSinkWithFeedbackMessenger : MessengerBase - { - private readonly IRoutingSinkWithFeedback display; - - /// - /// Create an instance of the class. - /// - /// - /// - /// - public IRoutingSinkWithFeedbackMessenger(string key, string messagePath, IRoutingSinkWithFeedback device) : base(key, messagePath, device) - { - display = device; - } - - /// - protected override void RegisterActions() - { - base.RegisterActions(); - - /*AddAction("/powerOn", (id, content) => display.PowerOn()); - AddAction("/powerOff", (id, content) => display.PowerOff()); - AddAction("/powerToggle", (id, content) => display.PowerToggle());*/ - - AddAction("/inputSelect", (id, content) => - { - var s = content.ToObject>(); - - var inputPort = display.InputPorts.FirstOrDefault(i => i.Key == s.Value); - - if (inputPort == null) - { - this.LogWarning("No input named {inputName} found for {deviceKey}", s, display.Key); - return; - } - - display.ExecuteSwitch(inputPort.Selector); - }); - - AddAction("/inputs", (id, content) => - { - var inputsList = display.InputPorts.Select(p => p.Key).ToList(); - - var messageObject = new MobileControlMessage - { - Type = MessagePath + "/inputs", - Content = JToken.FromObject(new - { - inputKeys = inputsList, - }) - }; - - AppServerController.SendMessageObject(messageObject); - }); - } - } -} \ No newline at end of file From 101d6c6e7990afa9935963e2b3e1cf4fb71bda9b Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 20 May 2026 13:30:42 -0600 Subject: [PATCH 068/161] feat: Enhance token validation in MobileControlWebsocketServer to handle missing tokens --- .../WebSocketServer/MobileControlWebsocketServer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 09b7dec1..af2cbe48 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -1154,10 +1154,12 @@ namespace PepperDash.Essentials.WebSocketServer byte[] body; - if (!UiClientContexts.TryGetValue(token, out UiClientContext clientContext)) + if (string.IsNullOrEmpty(token) || !UiClientContexts.TryGetValue(token, out UiClientContext clientContext)) { - var message = "Token invalid or has expired"; - res.StatusCode = 401; + var message = string.IsNullOrEmpty(token) + ? "Token is required" + : "Token invalid or has expired"; + res.StatusCode = string.IsNullOrEmpty(token) ? 400 : 401; res.ContentType = "application/json"; this.LogVerbose("{message}", message); body = Encoding.UTF8.GetBytes(message); From 0244b0e585d90dcf2b2927ef273f9d95e773a461 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 27 May 2026 19:04:32 -0600 Subject: [PATCH 069/161] feat: Add IBasicVolumeControlsMessenger and update MessengerFactoryRegistry for volume control integration --- .../IBasicVolumeWithFeedbackAdvanced.cs | 5 +++++ .../Devices/LevelControlListItem.cs | 2 +- ...nger.cs => IBasicVolumeControlsMessenger.cs} | 17 +++++++++++++---- .../MessengerFactoryRegistry.cs | 10 +++++++++- 4 files changed, 28 insertions(+), 6 deletions(-) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{DeviceVolumeMessenger.cs => IBasicVolumeControlsMessenger.cs} (88%) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedbackAdvanced.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedbackAdvanced.cs index f0a018c5..5baece21 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedbackAdvanced.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IBasicVolumeWithFeedbackAdvanced.cs @@ -14,5 +14,10 @@ namespace PepperDash.Essentials.Core /// Gets the volume level units /// eVolumeLevelUnits Units { get; } + + /// + /// Gets the maximum volume level + /// + int MaxVolumeLevel { get; } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs b/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs index 04f14ac9..229197f3 100644 --- a/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs @@ -8,7 +8,7 @@ namespace PepperDash.Essentials.Core; /// /// Represents a level control item in a list, which can be used to control volume or mute functionality. /// -public class LevelControlListItem : AudioControlListItemBase +public class LevelControlListItem : AudioConptrolListItemBase { /// diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IBasicVolumeControlsMessenger.cs similarity index 88% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IBasicVolumeControlsMessenger.cs index ff38a784..91b01f78 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IBasicVolumeControlsMessenger.cs @@ -9,19 +9,19 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials.AppServer.Messengers { /// - /// Represents a DeviceVolumeMessenger + /// Represents a IBasicVolumeControlsMessenger /// - public class DeviceVolumeMessenger : MessengerBase + public class IBasicVolumeControlsMessenger : MessengerBase { private readonly IBasicVolumeControls device; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The key. /// The message path. /// The device. - public DeviceVolumeMessenger(string key, string messagePath, IBasicVolumeControls device) + public IBasicVolumeControlsMessenger(string key, string messagePath, IBasicVolumeControls device) : base(key, messagePath, device) { this.device = device; @@ -40,6 +40,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { Volume = new Volume { + Label = device.Name, Level = feedbackDevice?.VolumeLevelFeedback.IntValue ?? -1, Muted = feedbackDevice?.MuteFeedback.BoolValue ?? false, HasMute = true, // assume all devices have mute for now @@ -50,6 +51,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { messageObj.Volume.RawValue = volumeAdvanced.RawVolumeLevel.ToString(); messageObj.Volume.Units = volumeAdvanced.Units; + messageObj.Volume.MaxLevel = volumeAdvanced.MaxVolumeLevel; } PostStatusMessage(messageObj, id); @@ -155,6 +157,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { message.Volume.RawValue = volumeAdvanced.RawVolumeLevel.ToString(); message.Volume.Units = volumeAdvanced.Units; + message.Volume.MaxLevel = volumeAdvanced.MaxVolumeLevel; } PostStatusMessage(JToken.FromObject(message)); @@ -220,5 +223,11 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("units", NullValueHandling = NullValueHandling.Ignore)] public eVolumeLevelUnits? Units { get; set; } + + /// + /// Gets or sets the MaxLevel. Defaults to 65535, which is the standard for Crestron devices. This is used by the mobile control app to determine how to scale the volume level for display and input purposes. + /// + [JsonProperty("maxLevel", NullValueHandling = NullValueHandling.Ignore)] + public int MaxLevel { get; set; } = 65535; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index e3454031..1b81a5b0 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -14,6 +14,7 @@ using PepperDash.Essentials.Devices.Common.Codec; using PepperDash.Essentials.Devices.Common.VideoCodec; using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; using PepperDash.Essentials.Room.MobileControl; +using PepperDash.Essentials.RoomBridges; namespace PepperDash.Essentials { @@ -134,7 +135,7 @@ namespace PepperDash.Essentials // ── Audio / Video ───────────────────────────────────────────────────────── new MessengerFactoryEntry( typeof(IBasicVolumeControls), - (d, mp, ck) => new DeviceVolumeMessenger( + (d, mp, ck) => new IBasicVolumeControlsMessenger( $"{d.Key}-volume-{ck}", mp, (IBasicVolumeControls)d) ), new MessengerFactoryEntry( @@ -404,6 +405,13 @@ namespace PepperDash.Essentials (d, mp, ck) => new ILevelControlsMessenger( $"{d.Key}-levelControls-{ck}", mp, (ILevelControls)d) ), + + // ── Essentials Room ───────────────────────────────────────────────────────── + new MessengerFactoryEntry( + typeof(IEssentialsRoom), + (d, mp, ck) => new MobileControlEssentialsRoomBridge( + (IEssentialsRoom)d) + ), }; } } From cd89cf1cc91e0d5482bc4681a734b065a96b7db7 Mon Sep 17 00:00:00 2001 From: anthony-lopez-pd Date: Tue, 2 Jun 2026 09:55:27 -0400 Subject: [PATCH 070/161] fix: Correct typo in LevelControlListItem class name --- src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs b/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs index 229197f3..04f14ac9 100644 --- a/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs @@ -8,7 +8,7 @@ namespace PepperDash.Essentials.Core; /// /// Represents a level control item in a list, which can be used to control volume or mute functionality. /// -public class LevelControlListItem : AudioConptrolListItemBase +public class LevelControlListItem : AudioControlListItemBase { /// From a1617fbeb22828675627d7c72cc93db5a6b2809c Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 3 Jun 2026 12:22:06 -0600 Subject: [PATCH 071/161] fix: Remove unnecessary message from NotImplementedException in DevicePropertiesConverter --- src/PepperDash.Essentials.Core/Config/DeviceConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Config/DeviceConfig.cs b/src/PepperDash.Essentials.Core/Config/DeviceConfig.cs index 49994abc..8e38fcfa 100644 --- a/src/PepperDash.Essentials.Core/Config/DeviceConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/DeviceConfig.cs @@ -76,6 +76,6 @@ public class DevicePropertiesConverter : JsonConverter public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - throw new NotImplementedException("SOD OFF HOSER"); + throw new NotImplementedException(); } } \ No newline at end of file From 153922c3fefc44175ea1f7d4839dcacaf58f3ab6 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 3 Jun 2026 14:09:47 -0600 Subject: [PATCH 072/161] refactor: Remove commented-out Config properties from Device class and add spacing in EssentialsDevice class --- src/PepperDash.Core/Device.cs | 10 ---------- .../Devices/EssentialsDevice.cs | 1 + 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/PepperDash.Core/Device.cs b/src/PepperDash.Core/Device.cs index e1e9c066..709c712e 100644 --- a/src/PepperDash.Core/Device.cs +++ b/src/PepperDash.Core/Device.cs @@ -24,16 +24,6 @@ public class Device : IKeyName /// public bool Enabled { get; protected set; } - /// - /// A place to store reference to the original config object, if any. These values should - /// NOT be used as properties on the device as they are all publicly-settable values. - /// - //public DeviceConfig Config { get; private set; } - /// - /// Helper method to check if Config exists - /// - //public bool HasConfig { get { return Config != null; } } - List _PreActivationActions; List _PostActivationActions; diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs index 7a62d1c3..81cd0439 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs @@ -15,6 +15,7 @@ namespace PepperDash.Essentials.Core; [Description("The base Essentials Device Class")] public abstract class EssentialsDevice : Device { + /// /// Event that fires when the device has completed initialization. This is useful for any setup that needs to occur after all devices have been activated. /// From bb029221c0f6c65e25ae58c21b6027a894c76a86 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 3 Jun 2026 17:00:43 -0600 Subject: [PATCH 073/161] feat: Add DevelopmentServerAddress property for direct communication and update URL handling in MobileControl classes --- .../MobileControlTouchpanelController.cs | 14 ++++++++++++-- .../MobileControlTouchpanelProperties.cs | 9 +++++++++ .../MobileControlWebsocketServer.cs | 6 +++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs index 24b5e242..a8100e80 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -519,7 +519,7 @@ namespace PepperDash.Essentials.Touchpanel return false; }) ? csIpAddress.ToString() : processorIp; - var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$"); + var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app/?\?token=.+$"); if (match.Success) { string ipa = match.Groups[1].Value; @@ -583,7 +583,17 @@ namespace PepperDash.Essentials.Touchpanel /// public void SetAppUrl(string url) { - _appUrl = GetUrlWithCorrectIp(url); + if(localConfig.DevelopmentServerAddress != null) + { + url = Regex.Replace(url, @"^http://[^/]+", $"http://{localConfig.DevelopmentServerAddress}"); + this.LogInformation("Using development server IP, updated URL: {url}", url); + } + else + { + url = GetUrlWithCorrectIp(url); + } + + _appUrl = url; AppUrlFeedback.FireUpdate(); } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs index cf022c16..c6658f9f 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs @@ -16,6 +16,15 @@ public class MobileControlTouchpanelProperties : CrestronTouchpanelPropertiesCon [JsonProperty("useDirectServer")] public bool UseDirectServer { get; set; } = false; + /// + /// Gets or sets the DevelopmentServerIp. Optional. If UseDirectServer is true, + /// this IP address will be used for direct communication between the mobile control and a development server running on the same local network. + /// This allows development using a Vite server for hot module replacement (HMR) and live reloading during development. + /// Specifying this property will override the default behavior of mobile control. + /// + [JsonProperty("developmentServerAddress", NullValueHandling = NullValueHandling.Ignore)] + public string DevelopmentServerAddress { get; set; } + /// /// Gets or sets the ZoomRoomController diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index af2cbe48..0334df39 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -403,11 +403,11 @@ namespace PepperDash.Essentials.WebSocketServer ip = csIpAddress.ToString(); } - var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"; + var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app/?token={touchpanel.Key}"; this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key); - touchpanel.Touchpanel.SetAppUrl($"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}"); } } @@ -1228,7 +1228,7 @@ namespace PepperDash.Essentials.WebSocketServer Config = _parent.GetConfigWithPluginVersion(), CodeExpires = new DateTime().AddYears(1), UserCode = bridge.UserCode, - UserAppUrl = string.Format("http://{0}:{1}/mc/app", + UserAppUrl = string.Format("http://{0}:{1}/mc/app/", CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), Port), WebSocketUrl = wsUrl, From a90b5da61bc8d33038f9fdb4f421b2d89dcd6fe5 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 4 Jun 2026 17:24:45 -0600 Subject: [PATCH 074/161] feat: Extend IDspPresets interface to inherit from IKeyed --- src/PepperDash.Essentials.Core/Devices/IDspPresets.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Devices/IDspPresets.cs b/src/PepperDash.Essentials.Core/Devices/IDspPresets.cs index bfe81658..96aeff92 100644 --- a/src/PepperDash.Essentials.Core/Devices/IDspPresets.cs +++ b/src/PepperDash.Essentials.Core/Devices/IDspPresets.cs @@ -6,7 +6,7 @@ namespace PepperDash.Essentials.Core /// /// Defines the contract for IDspPresets /// - public interface IDspPresets + public interface IDspPresets : IKeyed { /// /// Gets the Presets From 48e64a291f15308c53ec3dc6f0431923efaa623e Mon Sep 17 00:00:00 2001 From: anthony-lopez-pd Date: Fri, 5 Jun 2026 16:02:23 -0400 Subject: [PATCH 075/161] feat(videocodec): add IsCohost to Participant model Participant exposed IsHost/IsMyself but no co-host role. Several SDKs (e.g. the ZRC SDK behind epi-zoom-room) report a distinct cohost; add an IsCohost flag so codecs can surface the Me/Host/Cohost role the mobile control UI expects. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../VideoCodec/Interfaces/Participant.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs index 2d266a95..e8c46f7a 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs @@ -15,6 +15,11 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces /// public bool IsHost { get; set; } + /// + /// Gets or sets the IsCohost + /// + public bool IsCohost { get; set; } + /// /// Gets or sets the IsMyself /// From a27f60466034599bbc34c551e25a6d4a90e58414 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 8 Jun 2026 14:33:47 -0600 Subject: [PATCH 076/161] feat: Add IHasCameraMuteMessenger and IHasCameraPresetsMessenger for camera control messaging --- .../Messengers/IHasCameraMuteMessenger.cs | 89 +++++++++++++++++ .../Messengers/IHasCameraPresetsMessenger.cs | 98 +++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCameraMuteMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCameraPresetsMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCameraMuteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCameraMuteMessenger.cs new file mode 100644 index 00000000..c1a5340a --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCameraMuteMessenger.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Cameras; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasCameraMuteMessenger : MessengerBase + { + private readonly IHasCameraMute _cameraMute; + + /// + /// Initializes a new instance of the class. + /// + /// The key for the messenger. + /// The message path for the messenger. + /// The device implementing . + public IHasCameraMuteMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _cameraMute = device as IHasCameraMute ?? throw new ArgumentNullException(nameof(device)); + _cameraMute.CameraIsMutedFeedback.OutputChange += CameraIsMutedFeedback_OutputChange; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + AddAction("/cameraMuteStatus", (id, content) => SendFullStatus(id)); + + AddAction("/cameraMuteOn", (id, content) => _cameraMute.CameraMuteOn()); + AddAction("/cameraMuteOff", (id, content) => _cameraMute.CameraMuteOff()); + AddAction("/cameraMuteToggle", (id, content) => _cameraMute.CameraMuteToggle()); + } + + private void CameraIsMutedFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + try + { + PostStatusMessage(new IHasCameraMuteStateMessage + { + CameraIsMuted = e.BoolValue + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting camera mute state"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasCameraMuteStateMessage + { + CameraIsMuted = _cameraMute.CameraIsMutedFeedback.BoolValue + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending camera mute full status"); + } + } + } + + /// + /// State message for + /// + public class IHasCameraMuteStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets a value indicating whether the camera is muted. Null if unknown or not applicable. + /// + [JsonProperty("cameraIsMuted", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraIsMuted { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCameraPresetsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCameraPresetsMessenger.cs new file mode 100644 index 00000000..b7cc61eb --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCameraPresetsMessenger.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.Cameras; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IHasCameraPresetsMessenger : MessengerBase + { + private readonly IHasCameraPresets _cameraPresets; + + /// + /// Initializes a new instance of the class. + /// + /// The key for the messenger. + /// The message path for the messenger. + /// The device implementing . + public IHasCameraPresetsMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _cameraPresets = device as IHasCameraPresets ?? throw new ArgumentNullException(nameof(device)); + _cameraPresets.PresetsListHasChanged += PresetsListHasChanged; + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + AddAction("/presetsStatus", (id, content) => SendFullStatus(id)); + + AddAction("/recallPreset", (id, content) => + { + var msg = content.ToObject>(); + _cameraPresets.PresetSelect(msg.Value); + }); + + AddAction("/savePreset", (id, content) => + { + var msg = content.ToObject>(); + _cameraPresets.PresetStore(msg.Value, string.Empty); + }); + } + + private void PresetsListHasChanged(object sender, EventArgs e) + { + try + { + PostStatusMessage(new IHasCameraPresetsStateMessage + { + Presets = _cameraPresets.Presets + }); + } + catch (Exception ex) + { + this.LogError(ex, "Error posting camera presets state"); + } + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IHasCameraPresetsStateMessage + { + Presets = _cameraPresets.Presets + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending camera presets full status"); + } + } + } + + /// + /// State message for + /// + public class IHasCameraPresetsStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets the list of camera presets. + /// + [JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)] + public List Presets { get; set; } + } +} From 041bb5c50ea30cd0b2578b5aca9e93f6afdaacdb Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 8 Jun 2026 16:57:34 -0600 Subject: [PATCH 077/161] feat: Add null or empty check for AppUrl in SetAppUrl method --- .../Touchpanel/MobileControlTouchpanelController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs index a8100e80..55d298d6 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -583,6 +583,12 @@ namespace PepperDash.Essentials.Touchpanel /// public void SetAppUrl(string url) { + if (string.IsNullOrEmpty(url)) + { + this.LogInformation("AppUrl is null or empty, skipping SetAppUrl"); + return; + } + if(localConfig.DevelopmentServerAddress != null) { url = Regex.Replace(url, @"^http://[^/]+", $"http://{localConfig.DevelopmentServerAddress}"); From b2ddce2cc63b34972f5ab7b1f57b6a92b08d2ba2 Mon Sep 17 00:00:00 2001 From: anthony-lopez-pd Date: Tue, 9 Jun 2026 14:30:23 -0400 Subject: [PATCH 078/161] fix(videocodec): correct join-map direction labels in VideoCodecControllerJoinMap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capability metadata only — no wiring/behavior change. Verified against the actual LinkToApi wiring and a SIMPL EISC bridge signal dump: - CameraPresetSelect (a121), FarEndPresetSelect (a122): ToSIMPL -> ToFromSIMPL (driven as input via SetUShortSigAction, also fed back) - SelectedContactMethodCount (a102): FromSIMPL -> ToSIMPL ("Reports..." feedback) - CurrentDialString (s1): ToSIMPL -> ToFromSIMPL (read as the dial input by ManualDial, echoed back as FB) - Participant audio/video/pin toggles (d501/801/1101): ToSIMPL -> FromSIMPL (commands) Makes the generated join-map docs accurate for every video-codec EPI. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../JoinMaps/VideoCodecControllerJoinMap.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs index b691bbdb..cc004fc6 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs @@ -1226,7 +1226,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; new JoinMetadata { Description = "Toggles the participant's audio mute status", - JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); @@ -1240,7 +1240,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; new JoinMetadata { Description = "Toggles the participant's video mute status", - JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); @@ -1254,7 +1254,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; new JoinMetadata { Description = "Toggles the participant's pin status", - JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital }); @@ -1402,7 +1402,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; new JoinMetadata { Description = "Reports the number of contact methods for the selected contact", - JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); @@ -1448,7 +1448,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; new JoinMetadata { Description = "Camera Preset Select", - JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); @@ -1462,7 +1462,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; new JoinMetadata { Description = "Far End Preset Preset Select", - JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog }); @@ -1578,7 +1578,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; new JoinMetadata { Description = "Value to dial when ManualDial digital join is pulsed", - JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Serial }); From f2f4662c8d50c195420e75f57145cdd22f996939 Mon Sep 17 00:00:00 2001 From: anthony-lopez-pd Date: Tue, 9 Jun 2026 15:45:39 -0400 Subject: [PATCH 079/161] fix: remove dead SelectLayout and AvailableLayoutsFb serial-142 joins Both entries were forward-looking stubs: IHasCodecLayouts has no string-based layout-select method, so neither join was ever wired in LinkVideoCodecCameraLayoutsToApi. String-based layout selection is Zoom-specific (eLayoutStyle enum) and lives on serial 215 in the EPI. Co-Authored-By: Claude Sonnet 4.6 --- .../JoinMaps/VideoCodecControllerJoinMap.cs | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs index cc004fc6..ac494a5f 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs @@ -1757,35 +1757,6 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; JoinType = eJoinType.Serial }); - [JoinName("AvailableLayoutsFb")] - public JoinDataComplete AvailableLayoutsFb = new JoinDataComplete( - new JoinData - { - JoinNumber = 142, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "xSig of all available layouts", - JoinCapabilities = eJoinCapabilities.ToSIMPL, - JoinType = eJoinType.Serial - }); - - [JoinName("SelectLayout")] - public JoinDataComplete SelectLayout = new JoinDataComplete( - new JoinData - { - JoinNumber = 142, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Select Layout by string", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Serial - }); - - /// /// Current Participants XSig /// From 4058040330efcde65fbb271a45dbaa60ff551027 Mon Sep 17 00:00:00 2001 From: anthony-lopez-pd Date: Tue, 9 Jun 2026 16:20:58 -0400 Subject: [PATCH 080/161] fix: remove dead DirectoryLineSelected join; fix SelfviewPositionFb description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DirectoryLineSelected (digital 101, FromSIMPL) had no handler anywhere in VideoCodecBase.cs — directory selection is driven by analog 101 (DirectorySelectRow). DirectoryEntryIsContact (ToSIMPL) already documents the output side of that join. Removed the dead inbound entry. SelfviewPositionFb (serial 211) description "advance selfview position" read like a command; updated to "Current selfview PIP position string FB" to accurately reflect that it is a ToSIMPL feedback. Co-Authored-By: Claude Sonnet 4.6 --- .../JoinMaps/VideoCodecControllerJoinMap.cs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs index ac494a5f..9c434cab 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs @@ -442,22 +442,6 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; JoinType = eJoinType.Digital }); - /// - /// Directory Line Selected - /// - [JoinName("DirectoryLineSelected")] - public JoinDataComplete DirectoryLineSelected = new JoinDataComplete( - new JoinData - { - JoinNumber = 101, - JoinSpan = 1 - }, - new JoinMetadata - { - Description = "Directory Line Selected FB", - JoinCapabilities = eJoinCapabilities.FromSIMPL, - JoinType = eJoinType.Digital - }); /// /// Directory Is Root @@ -1873,7 +1857,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; }, new JoinMetadata { - Description = "advance selfview position", + Description = "Current selfview PIP position string FB", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial }); From dff50ea5447cb7ca113591dee2b960e33cc6f2a0 Mon Sep 17 00:00:00 2001 From: anthony-lopez-pd Date: Tue, 9 Jun 2026 16:58:30 -0400 Subject: [PATCH 081/161] fix: correct PhoneHookState (digital 72) feedback description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PhoneHookState (ToSIMPL) was labeled "Dial Phone" — a copy of the DialPhone input description on the same join. Changed to "Phone hook state FB" to accurately describe the feedback. Co-Authored-By: Claude Sonnet 4.6 --- .../Bridges/JoinMaps/VideoCodecControllerJoinMap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs index 9c434cab..0b2cfbf9 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs @@ -347,7 +347,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; }, new JoinMetadata { - Description = "Dial Phone", + Description = "Phone hook state FB", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }); From 72c948e760162897d25c03dd9825a1a03b120883 Mon Sep 17 00:00:00 2001 From: anthony-lopez-pd Date: Tue, 9 Jun 2026 17:16:23 -0400 Subject: [PATCH 082/161] fix: disambiguate DirectorySelectRow (101) and feedback (104) descriptions Both analog 101 (FromSIMPL select) and analog 104 (ToSIMPL feedback echo) were described "Directory Select Row and Feedback", implying the feedback was on 101. Clarified: 101 = "Directory Select Row"; 104 = "Directory Selected Row FB". Co-Authored-By: Claude Sonnet 4.6 --- .../Bridges/JoinMaps/VideoCodecControllerJoinMap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs index 0b2cfbf9..05be3bfa 100644 --- a/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Bridges/JoinMaps/VideoCodecControllerJoinMap.cs @@ -1370,7 +1370,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; }, new JoinMetadata { - Description = "Directory Select Row and Feedback", + Description = "Directory Select Row", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Analog }); @@ -1413,7 +1413,7 @@ namespace PepperDash.Essentials.Core.Bridges.JoinMaps; }, new JoinMetadata { - Description = "Directory Select Row and Feedback", + Description = "Directory Selected Row FB", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog }); From c306fd717eecd86a921948de7a509cf57b52605a Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 10 Jun 2026 22:58:57 -0600 Subject: [PATCH 083/161] refactor: remove unused GetInterfaces method and streamline constructor initialization --- .../Messengers/MessengerBase.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index fbbaa5e5..88c61331 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -84,19 +84,8 @@ namespace PepperDash.Essentials.AppServer.Messengers : this(key, messagePath) { _device = device; - - _deviceInterfaces = GetInterfaces(_device as Device); } - /// - /// Gets the interfaces implmented on the device - /// - /// - /// - private List GetInterfaces(Device device) - { - return device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); - } /// /// Registers this messenger with appserver controller @@ -262,6 +251,8 @@ namespace PepperDash.Essentials.AppServer.Messengers message.Name = _device.Name; + message.MessageBasePath = MessagePath; + var token = JToken.FromObject(message); PostStatusMessage(token, MessagePath, clientId); From 22f5558e39125a16db875e601e29c1987ec55ca1 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 11 Jun 2026 16:44:41 -0600 Subject: [PATCH 084/161] feat: add IAudioCodecInfoMessenger for messaging bridge implementation --- .../Interfaces/IAudioCodecPhonebook.cs | 15 ++++ .../Messengers/IAudioCodecInfoMessenger.cs | 71 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecInfoMessenger.cs diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs new file mode 100644 index 00000000..31021fbe --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs @@ -0,0 +1,15 @@ + + + +using System.Collections.Generic; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.Devices.Common.AudioCodec; + +public interface IAudioCodecPhonebook : IHasDialer +{ + /// + /// Gets the list of phonebook entries for the device. + /// + List PhonebookEntries { get; } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecInfoMessenger.cs new file mode 100644 index 00000000..895b5016 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecInfoMessenger.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.AudioCodec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IAudioCodecInfoMessenger : MessengerBase + { + private readonly IAudioCodecInfo _codecInfo; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + public IAudioCodecInfoMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _codecInfo = device as IAudioCodecInfo ?? throw new ArgumentNullException(nameof(device)); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/codecInfoStatus", (id, content) => SendFullStatus(id)); + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IAudioCodecInfoStateMessage + { + PhoneNumber = _codecInfo.CodecInfo?.PhoneNumber + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending audio codec info full status"); + } + } + } + + /// + /// State message for + /// + public class IAudioCodecInfoStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets the phone number of the audio codec + /// + [JsonProperty("phoneNumber", NullValueHandling = NullValueHandling.Ignore)] + public string PhoneNumber { get; set; } + } +} From 2dd99da67f0c83fc50ac0ecfea82fc2e251da861 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 11 Jun 2026 16:55:03 -0600 Subject: [PATCH 085/161] feat: implement IAudioCodecPhonebookMessenger for phonebook messaging functionality --- .../Interfaces/IAudioCodecPhonebook.cs | 29 +++++- .../IAudioCodecPhonebookMessenger.cs | 94 +++++++++++++++++++ .../Messengers/IHasDialerMessenger.cs | 1 - 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecPhonebookMessenger.cs diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs index 31021fbe..253680d7 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs @@ -1,15 +1,38 @@ - - - +using Newtonsoft.Json; using System.Collections.Generic; using PepperDash.Essentials.Devices.Common.Codec; namespace PepperDash.Essentials.Devices.Common.AudioCodec; +/// +/// Defines the contract for a device that has a phonebook. +/// This is used to provide a common interface for devices that have phonebook functionality +/// public interface IAudioCodecPhonebook : IHasDialer { + /// + /// Sets a phonebook entry at the specified index. The implementation of this method is up to the device, but it should update the phonebook entry at the specified index with the provided name and number. + /// + /// + /// + /// + void SetPhonebookEntry(int index, string name, string number); + /// /// Gets the list of phonebook entries for the device. /// List PhonebookEntries { get; } +} + +/// +/// Defines a phonebook entry for the audio codec phonebook. +/// This is used to provide a common data structure for phonebook entries across different devices. +/// +public class CodecPhonebookEntry +{ + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("number")] + public string Number { get; set; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecPhonebookMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecPhonebookMessenger.cs new file mode 100644 index 00000000..acf5fcff --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecPhonebookMessenger.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.AudioCodec; +using System.Collections.Generic; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for devices implementing + /// + public class IAudioCodecPhonebookMessenger : MessengerBase + { + private readonly IAudioCodecPhonebook _phonebook; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + public IAudioCodecPhonebookMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _phonebook = device as IAudioCodecPhonebook ?? throw new ArgumentNullException(nameof(device)); + } + + /// + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/phonebookStatus", (id, content) => SendFullStatus(id)); + + AddAction("/setEntry", (id, content) => + { + var entry = content.ToObject(); + _phonebook.SetPhonebookEntry(entry.Index, entry.Name, entry.Number); + SendFullStatus(); + }); + } + + private void SendFullStatus(string id = null) + { + try + { + var state = new IAudioCodecPhonebookStateMessage + { + PhonebookEntries = _phonebook.PhonebookEntries + }; + + Task.Run(() => PostStatusMessage(state, id)); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending phonebook full status"); + } + } + } + + /// + /// State message for + /// + public class IAudioCodecPhonebookStateMessage : DeviceStateMessageBase + { + /// + /// Gets or sets the list of phonebook entries + /// + [JsonProperty("phonebookEntries", NullValueHandling = NullValueHandling.Ignore)] + public List PhonebookEntries { get; set; } + } + + /// + /// Content model for the setEntry action + /// + public class SetPhonebookEntryContent + { + [JsonProperty("index")] + public int Index { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("number")] + public string Number { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs index 143f953e..63a83270 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs @@ -94,6 +94,5 @@ namespace PepperDash.Essentials.AppServer.Messengers /// [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] public bool? IsInCall { get; set; } - } } From 1b2f86eb439b895a57b6fc5e1b5d858879f8867e Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 12 Jun 2026 10:53:04 -0600 Subject: [PATCH 086/161] feat: add UserInterfaces and TouchpanelWrapperApp properties to VersionData class --- .../Config/Essentials/EssentialsConfig.cs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index d9a5918f..c98430cd 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -100,6 +100,20 @@ public class VersionData [JsonProperty("essentials")] public NugetVersion Essentials { get; set; } + /// + /// Gets or sets the list of UserInterfaces + /// + [JsonProperty("userInterfaces")] + public List UserInterfaces { get; set; } + + /// + /// Gets or sets the TouchpanelWrapperApp version + /// This is the .ch5z app that gets loaded to a Crestron touch panel that allows it + /// to run an HTML5 user interface. + /// + [JsonProperty("touchpanelWrapperApp")] + public AppVersion TouchpanelWrapperApp { get; set; } + /// /// Gets or sets the list of Packages /// @@ -111,10 +125,27 @@ public class VersionData /// public VersionData() { + UserInterfaces = new List(); Packages = new List(); } } +public class AppVersion +{ + /// + /// Gets or sets the Version + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// Gets or sets the RepoUrl + /// + [JsonProperty("repoUrl")] + public string RepoUrl { get; set; } +} + + /// /// Represents a NugetVersion /// @@ -131,10 +162,16 @@ public class NugetVersion /// [JsonProperty("packageId")] public string PackageId { get; set; } + + /// + /// Gets or sets the RepoUrl + /// + [JsonProperty("repoUrl")] + public string RepoUrl { get; set; } } /// -/// +/// Represents the configuration for a system and its template /// public class SystemTemplateConfigs { From 4403e91b796d15305d1bec5af048ae0d1f49b6ce Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 12 Jun 2026 11:17:11 -0600 Subject: [PATCH 087/161] fix: correct UserInterfaces type and initialize TouchpanelWrapperApp in VersionData constructor --- .../Config/Essentials/EssentialsConfig.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index c98430cd..0fb0c93e 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -125,7 +125,8 @@ public class VersionData /// public VersionData() { - UserInterfaces = new List(); + UserInterfaces = new List(); + TouchpanelWrapperApp = new AppVersion(); Packages = new List(); } } From 27b3d9cf58fb9c9e8c7481e42016b9c504da229b Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Mon, 15 Jun 2026 13:46:32 -0400 Subject: [PATCH 088/161] feat: updates IAudioCodecPhonebook interface --- .../Interfaces/IAudioCodecPhonebook.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs index 253680d7..3391f462 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IAudioCodecPhonebook.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; using System.Collections.Generic; using PepperDash.Essentials.Devices.Common.Codec; @@ -10,6 +11,11 @@ namespace PepperDash.Essentials.Devices.Common.AudioCodec; /// public interface IAudioCodecPhonebook : IHasDialer { + /// + /// Raised when the list of phonebook entries changes. The event args contain the updated list of entries. + /// + event EventHandler ListChanged; + /// /// Sets a phonebook entry at the specified index. The implementation of this method is up to the device, but it should update the phonebook entry at the specified index with the provided name and number. /// @@ -18,6 +24,12 @@ public interface IAudioCodecPhonebook : IHasDialer /// void SetPhonebookEntry(int index, string name, string number); + /// + /// Dials the phonebook entry at the specified index. + /// + /// + void DialPhonebookEntry(int index); + /// /// Gets the list of phonebook entries for the device. /// @@ -35,4 +47,24 @@ public class CodecPhonebookEntry [JsonProperty("number")] public string Number { get; set; } +} + +/// +/// Provides the updated list of phonebook entries for the event. +/// +public class PhonebookListChangedEventArgs : EventArgs +{ + /// + /// Gets the updated list of phonebook entries. + /// + public List Entries { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The updated list of phonebook entries. + public PhonebookListChangedEventArgs(List entries) + { + Entries = entries; + } } \ No newline at end of file From fb0afc8c412b0724c34d9309ec80cc54b9ca2d61 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 16 Jun 2026 08:57:31 -0600 Subject: [PATCH 089/161] feat: implement DeviceActionMessenger for executing device actions via JSON API --- .../Devices/DeviceJsonApi.cs | 3 ++ .../Messengers/DeviceActionMessenger.cs | 34 +++++++++++++++++++ .../MobileControlSystemController.cs | 6 ++++ 3 files changed, 43 insertions(+) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceActionMessenger.cs diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs index 6ba640d4..f4de34e4 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs @@ -393,16 +393,19 @@ public class DeviceActionWrapper /// /// The key of the device to call the method on /// + [JsonProperty("deviceKey")] public string DeviceKey { get; set; } /// /// The name of the method to call /// + [JsonProperty("methodName")] public string MethodName { get; set; } /// /// The parameters to pass to the method. This should be an array of objects matching the parameters of the method being called. If the method has no parameters, this can be omitted or set to null. /// + [JsonProperty("params")] public object[] Params { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceActionMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceActionMessenger.cs new file mode 100644 index 00000000..42f95138 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceActionMessenger.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.AppServer.Messengers; + +/// +/// Messenger that accepts generic device actions at /action/executeAction +/// and executes them using DeviceJsonApi.DoDeviceAction +/// +public class DeviceActionMessenger : MessengerBase +{ + public DeviceActionMessenger(string key, string messagePath) + : base(key, messagePath) + { + } + + protected override void RegisterActions() + { + AddAction("/executeAction", (id, content) => + { + var action = content.ToObject(); + + if (action == null) + { + this.LogWarning("Received null DeviceActionWrapper"); + return; + } + + DeviceJsonApi.DoDeviceAction(action); + }); + } +} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 5e3314aa..683877fd 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -242,6 +242,12 @@ namespace PepperDash.Essentials AddPreActivationAction(() => LinkSystemMonitorToAppServer()); + AddPreActivationAction(() => + { + var actionMessenger = new DeviceActionMessenger("deviceActionMessenger-" + Key, "/action"); + AddDeviceMessenger(actionMessenger); + }); + AddPreActivationAction(() => AddWebApiPaths()); AddPreActivationAction(() => From 0098673a5ece4ae83f050603f5bcc418166064a3 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 16 Jun 2026 16:14:07 -0600 Subject: [PATCH 090/161] feat: enhance audio codec phonebook functionality and messaging integration --- .../Devices/DeviceJsonApi.cs | 136 +++++++++--------- .../AudioCodec/MockAC/MockAC.cs | 47 +++++- .../IAudioCodecPhonebookMessenger.cs | 9 +- .../MessengerFactoryRegistry.cs | 10 ++ 4 files changed, 135 insertions(+), 67 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs index f4de34e4..b2565b3d 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs @@ -281,84 +281,92 @@ public class DeviceJsonApi /// public static object FindObjectOnPath(string deviceObjectPath) { - var path = deviceObjectPath.Split('.'); - - var dev = DeviceManager.GetDeviceForKey(path[0]); - if (dev == null) + try { - Debug.LogMessage(LogEventLevel.Information, "Device {0} not found", path[0]); - return null; - } + var path = deviceObjectPath.Split('.'); - // loop through any dotted properties - object obj = dev; - if (path.Length > 1) - { - for (int i = 1; i < path.Length; i++) + var dev = DeviceManager.GetDeviceForKey(path[0]); + if (dev == null) { - var objName = path[i]; - string indexStr = null; - var indexOpen = objName.IndexOf('['); - if (indexOpen != -1) - { - var indexClose = objName.IndexOf(']'); - if (indexClose == -1) - { - Debug.LogMessage(LogEventLevel.Information, dev, "ERROR Unmatched index brackets"); - return null; - } - // Get the index and strip quotes if any - indexStr = objName.Substring(indexOpen + 1, indexClose - indexOpen - 1).Replace("\"", ""); - objName = objName.Substring(0, indexOpen); - Debug.LogMessage(LogEventLevel.Information, dev, " Checking for collection '{0}', index '{1}'", objName, indexStr); - } + Debug.LogMessage(LogEventLevel.Information, "Device {0} not found", path[0]); + return null; + } - Type oType = obj.GetType(); - var prop = oType.GetProperty(objName); - if (prop == null) + // loop through any dotted properties + object obj = dev; + if (path.Length > 1) + { + for (int i = 1; i < path.Length; i++) { - Debug.LogMessage(LogEventLevel.Information, dev, "Property {0} not found on {1}", objName, path[i - 1]); - return null; - } - // if there's an index, try to get the property - if (indexStr != null) - { - if (!typeof(ICollection).IsAssignableFrom(prop.PropertyType)) + var objName = path[i]; + string indexStr = null; + var indexOpen = objName.IndexOf('['); + if (indexOpen != -1) { - Debug.LogMessage(LogEventLevel.Information, dev, "Property {0} is not collection", objName); - return null; - } - var collection = prop.GetValue(obj, null) as ICollection; - // Get the indexed items "property" - var indexedPropInfo = prop.PropertyType.GetProperty("Item"); - // These are the parameters for the indexing. Only care about one - var indexParams = indexedPropInfo.GetIndexParameters(); - if (indexParams.Length > 0) - { - Debug.LogMessage(LogEventLevel.Information, " Indexed, param type: {0}", indexParams[0].ParameterType.Name); - var properParam = Convert.ChangeType(indexStr, indexParams[0].ParameterType, - System.Globalization.CultureInfo.InvariantCulture); - try + var indexClose = objName.IndexOf(']'); + if (indexClose == -1) { - obj = indexedPropInfo.GetValue(collection, new object[] { properParam }); - } - // if the index is bad, catch it here. - catch (TargetInvocationException e) - { - if (e.InnerException is ArgumentOutOfRangeException) - Debug.LogMessage(LogEventLevel.Information, " Index Out of range"); - else if (e.InnerException is KeyNotFoundException) - Debug.LogMessage(LogEventLevel.Information, " Key not found"); + Debug.LogMessage(LogEventLevel.Information, dev, "ERROR Unmatched index brackets"); return null; } + // Get the index and strip quotes if any + indexStr = objName.Substring(indexOpen + 1, indexClose - indexOpen - 1).Replace("\"", ""); + objName = objName.Substring(0, indexOpen); + Debug.LogMessage(LogEventLevel.Information, dev, " Checking for collection '{0}', index '{1}'", objName, indexStr); } + Type oType = obj.GetType(); + var prop = oType.GetProperty(objName); + if (prop == null) + { + Debug.LogMessage(LogEventLevel.Information, dev, "Property {0} not found on {1}", objName, path[i - 1]); + return null; + } + // if there's an index, try to get the property + if (indexStr != null) + { + if (!typeof(ICollection).IsAssignableFrom(prop.PropertyType)) + { + Debug.LogMessage(LogEventLevel.Information, dev, "Property {0} is not collection", objName); + return null; + } + var collection = prop.GetValue(obj, null) as ICollection; + // Get the indexed items "property" + var indexedPropInfo = prop.PropertyType.GetProperty("Item"); + // These are the parameters for the indexing. Only care about one + var indexParams = indexedPropInfo.GetIndexParameters(); + if (indexParams.Length > 0) + { + Debug.LogMessage(LogEventLevel.Information, " Indexed, param type: {0}", indexParams[0].ParameterType.Name); + var properParam = Convert.ChangeType(indexStr, indexParams[0].ParameterType, + System.Globalization.CultureInfo.InvariantCulture); + try + { + obj = indexedPropInfo.GetValue(collection, new object[] { properParam }); + } + // if the index is bad, catch it here. + catch (TargetInvocationException e) + { + if (e.InnerException is ArgumentOutOfRangeException) + Debug.LogMessage(LogEventLevel.Information, " Index Out of range"); + else if (e.InnerException is KeyNotFoundException) + Debug.LogMessage(LogEventLevel.Information, " Key not found"); + return null; + } + } + + } + else + obj = prop.GetValue(obj, null); } - else - obj = prop.GetValue(obj, null); } + return obj; + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Error, e, "Error finding object on path {deviceObjectPath}", deviceObjectPath); + return null; } - return obj; } /// diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs index b32340af..b8913351 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using PepperDash.Core; using PepperDash.Essentials.Core; @@ -8,16 +9,35 @@ using Serilog.Events; namespace PepperDash.Essentials.Devices.Common.AudioCodec; -public class MockAC : AudioCodecBase +public class MockAC : AudioCodecBase, IAudioCodecPhonebook { + public event EventHandler ListChanged; + + public List PhonebookEntries { get; } + public MockAC(string key, string name, MockAcPropertiesConfig props) : base(key, name) { CodecInfo = new MockAudioCodecInfo(); CodecInfo.PhoneNumber = props.PhoneNumber; + + PhonebookEntries = new List + { + new() { Name = "Judge Chambers", Number = "5551001" }, + new() { Name = "Clerk Office", Number = "5551002" }, + new() { Name = "Court Reporter", Number = "5551003" }, + new() { Name = "Jury Room", Number = "5551004" }, + new() { Name = "Witness Room", Number = "5551005" }, + new() { Name = "Prosecution", Number = "5551006" }, + new() { Name = "Defense Counsel", Number = "5551007" }, + new() { Name = "Bailiff Station", Number = "5551008" }, + new() { Name = "Conference Room A", Number = "5551009" }, + new() { Name = "Conference Room B", Number = "5551010" }, + }; } + public override void Dial(string number) { if (!IsInCall) @@ -79,6 +99,29 @@ public class MockAC : AudioCodecBase Debug.LogMessage(LogEventLevel.Debug, this, "BEEP BOOP SendDTMF: {0}", s); } + public void SetPhonebookEntry(int index, string name, string number) + { + if (index < 0 || index >= PhonebookEntries.Count) + { + Debug.LogMessage(LogEventLevel.Debug, this, "SetPhonebookEntry: index {0} out of range", index); + return; + } + + PhonebookEntries[index] = new CodecPhonebookEntry { Name = name, Number = number }; + ListChanged?.Invoke(this, new PhonebookListChangedEventArgs(PhonebookEntries)); + } + + public void DialPhonebookEntry(int index) + { + if (index < 0 || index >= PhonebookEntries.Count) + { + Debug.LogMessage(LogEventLevel.Debug, this, "DialPhonebookEntry: index {0} out of range", index); + return; + } + + Dial(PhonebookEntries[index].Number); + } + /// /// /// diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecPhonebookMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecPhonebookMessenger.cs index acf5fcff..7d336f75 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecPhonebookMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IAudioCodecPhonebookMessenger.cs @@ -28,6 +28,8 @@ namespace PepperDash.Essentials.AppServer.Messengers : base(key, messagePath, device) { _phonebook = device as IAudioCodecPhonebook ?? throw new ArgumentNullException(nameof(device)); + + _phonebook.ListChanged += (sender, args) => SendFullStatus(); } /// @@ -43,7 +45,12 @@ namespace PepperDash.Essentials.AppServer.Messengers { var entry = content.ToObject(); _phonebook.SetPhonebookEntry(entry.Index, entry.Name, entry.Number); - SendFullStatus(); + }); + + AddAction("/dialEntry", (id, content) => + { + var request = content.ToObject>(); + _phonebook.DialPhonebookEntry(request.Value); }); } diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index 1b81a5b0..7124de56 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -251,6 +251,16 @@ namespace PepperDash.Essentials (d, mp, ck) => new IDialerCallStatusMessenger( $"{d.Key}-audioCodec-{ck}", (IDialerCallStatus)d, mp) ), + new MessengerFactoryEntry( + typeof(IAudioCodecInfo), + (d, mp, ck) => new IAudioCodecInfoMessenger( + $"{d.Key}-audioCodecInfo-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IAudioCodecPhonebook), + (d, mp, ck) => new IAudioCodecPhonebookMessenger( + $"{d.Key}-audioCodecPhonebook-{ck}", mp, d) + ), // ── Set-top box controls ────────────────────────────────────────────────── new MessengerFactoryEntry( From cff46532be768c53fbb6a0b067ed733520881826 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 16 Jun 2026 18:42:14 -0600 Subject: [PATCH 091/161] feat: update Dial method to limit active calls based on maxCalls --- .../AudioCodec/MockAC/MockAC.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs index b8913351..6749abf1 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/MockAC/MockAC.cs @@ -15,6 +15,8 @@ public class MockAC : AudioCodecBase, IAudioCodecPhonebook public List PhonebookEntries { get; } + private int maxCalls = 2; + public MockAC(string key, string name, MockAcPropertiesConfig props) : base(key, name) { @@ -40,7 +42,7 @@ public class MockAC : AudioCodecBase, IAudioCodecPhonebook public override void Dial(string number) { - if (!IsInCall) + if (ActiveCalls.Count < maxCalls) { Debug.LogMessage(LogEventLevel.Debug, this, "Dial: {0}", number); var call = new CodecActiveCallItem() From 747b595b5937ee292144c44d97eb1398f0a47e03 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 17 Jun 2026 22:25:38 -0600 Subject: [PATCH 092/161] feat: add actions to accept and reject calls in IHasDialerMessenger --- .../Messengers/IHasDialerMessenger.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs index 63a83270..e4b917b0 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDialerMessenger.cs @@ -47,6 +47,18 @@ namespace PepperDash.Essentials.AppServer.Messengers var s = content.ToObject>(); _dialer.SendDtmf(s.Value); }); + + AddAction("/acceptCall", (id, content) => + { + var callItem = content.ToObject(); + _dialer.AcceptCall(callItem); + }); + + AddAction("/rejectCall", (id, content) => + { + var callItem = content.ToObject(); + _dialer.RejectCall(callItem); + }); } private void Dialer_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e) @@ -55,7 +67,8 @@ namespace PepperDash.Essentials.AppServer.Messengers { var state = new IHasDialerStateMessage { - IsInCall = _dialer.IsInCall + IsInCall = _dialer.IsInCall, + CallItem = e.CallItem }; PostStatusMessage(state); @@ -94,5 +107,8 @@ namespace PepperDash.Essentials.AppServer.Messengers /// [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] public bool? IsInCall { get; set; } + + [JsonProperty("callItem", NullValueHandling = NullValueHandling.Ignore)] + public CodecActiveCallItem CallItem { get; set; } } } From 1588cc466781cab0e146f811c44f00ef588d0d21 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 18 Jun 2026 10:37:25 -0600 Subject: [PATCH 093/161] feat: add ICourtSession interface for court session management and recording control --- .../Recording/ICourtSession.cs | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/PepperDash.Essentials.Devices.Common/Recording/ICourtSession.cs diff --git a/src/PepperDash.Essentials.Devices.Common/Recording/ICourtSession.cs b/src/PepperDash.Essentials.Devices.Common/Recording/ICourtSession.cs new file mode 100644 index 00000000..b2096b30 --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/Recording/ICourtSession.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Plugin +{ + /// + /// Defines the contract for court session management including + /// session lifecycle, recording control, case management, and status feedback. + /// + public interface ICourtSession + { + #region Session Lifecycle + + /// + /// Create a new court session with the given title. + /// + bool CreateCourtSession(string title); + + /// + /// Get the active court session payload and cache it locally. + /// + bool GetActiveCourtSession(); + + /// + /// Check whether an active court session exists. + /// + bool HasActiveCourtSession(); + + /// + /// Edit the active court session title. + /// + bool EditCourtSessionTitle(string title); + + /// + /// Close the active court session. + /// + bool CloseCourtSession(); + + /// + /// Force close the active court session. + /// + bool ForceCloseCourtSession(); + + #endregion + + #region Recording Control + + /// + /// Start recording the active court session. + /// + bool StartRecording(); + + /// + /// Stop the current recording. + /// + bool StopRecording(); + + /// + /// Seal the current recording (protect/read-only). + /// + bool Seal(); + + /// + /// Unseal the current recording. + /// + bool Unseal(); + + /// + /// Get the current recording status from the API. + /// + bool GetRecordingStatus(); + + /// + /// Get the current seal status from the API. + /// + bool GetSealStatus(); + + #endregion + + + + + #region Status + + /// + /// Get current session information (case ID and name). + /// + Dictionary GetCurrentSessionInfo(); + + + /// + /// Get the activation status from the API. + /// + bool GetActivationStatus(); + + #endregion + + #region Feedbacks + + /// + /// True when currently recording. + /// + BoolFeedback IsRecording { get; } + + /// + /// True when the recording is sealed. + /// + BoolFeedback IsSealed { get; } + + /// + /// Recording duration in milliseconds. + /// + StringFeedback RecordingDurationMs { get; } + + /// + /// Recording duration formatted as HH:MM:SS. + /// + StringFeedback RecordingDurationFormatted { get; } + + /// + /// Current case ID for the active session. + /// + StringFeedback CurrentCaseId { get; } + + /// + /// Current case name for the active session. + /// + StringFeedback CurrentCaseName { get; } + + /// + /// Current recording session state (Idle, Recording, Sealed, Disconnected, Error). + /// + StringFeedback CurrentState { get; } + + /// + /// Error message if session is in error state. + /// + StringFeedback ErrorMessage { get; } + + /// + /// API heartbeat counter for diagnostics. + /// + IntFeedback HeartbeatCounter { get; } + + /// + /// True when CourtCapture reports an active court session. + /// + BoolFeedback HasActiveCourtSessionFeedback { get; } + + /// + /// Last command result envelope as JSON string for UI binding. + /// + StringFeedback LastCommandResult { get; } + + #endregion + + #region Events + + /// + /// Raised when case notes are updated. + /// + event EventHandler CaseNotesUpdated; + + /// + /// Raised when quick links are updated. + /// + event EventHandler QuickLinksUpdated; + + #endregion + } +} From 846fd711c57e49d6ed73d90afa2dbd3db64a2ea0 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 18 Jun 2026 10:57:17 -0600 Subject: [PATCH 094/161] fix: update namespace for ICourtSession interface to correct location --- .../Recording/ICourtSession.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Recording/ICourtSession.cs b/src/PepperDash.Essentials.Devices.Common/Recording/ICourtSession.cs index b2096b30..34411a21 100644 --- a/src/PepperDash.Essentials.Devices.Common/Recording/ICourtSession.cs +++ b/src/PepperDash.Essentials.Devices.Common/Recording/ICourtSession.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Devices.Common.Recording { /// /// Defines the contract for court session management including From be545bbfa4ced7b9ad558080323d0153d0413ea7 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 18 Jun 2026 16:41:47 -0600 Subject: [PATCH 095/161] fix: format constructor for IDeviceInfoProviderMessenger for improved readability --- .../Messengers/IDeviceInfoProviderMessenger.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs index 0bad9a89..fdf18a55 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs @@ -30,7 +30,8 @@ namespace PepperDash.Essentials.AppServer.Messengers /// A unique identifier for the messenger instance. /// The path used for sending and receiving messages. /// An implementation of that provides device-specific information. - public IDeviceInfoProviderMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device) + public IDeviceInfoProviderMessenger(string key, string messagePath, IDeviceInfoProvider device) + : base(key, messagePath, device as Device) { _deviceInfoProvider = device; From b65f895e06883c36f01ef3ea016e832da0fe06e5 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 22 Jun 2026 21:30:47 -0600 Subject: [PATCH 096/161] feat: implement Routing Feedback WebSocket service and associated request handlers --- .../Logging/DebugWebsocketSink.cs | 14 + .../Room/Interfaces.cs | 6 +- .../Routing/ICurrentSources.cs | 1 + .../Web/EssentialsWebApi.cs | 5 + .../DebugSessionRequestHandler.cs | 125 ++++- .../RoutingFeedbackSessionRequestHandler.cs | 233 ++++++++++ .../Web/RoutingFeedbackWebsocket.cs | 433 ++++++++++++++++++ 7 files changed, 812 insertions(+), 5 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs create mode 100644 src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 6214505b..3027640f 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -75,6 +75,20 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed /// public bool IsRunning { get => _httpsServer?.IsListening ?? false; } + /// + /// Gets a value indicating whether there are active WebSocket connections. + /// + public bool HasActiveConnections + { + get + { + if (_httpsServer == null || !_httpsServer.IsListening) return false; + var service = _httpsServer.WebSocketServices[_path]; + if (service == null) return false; + return service.Sessions.Count > 0; + } + } + /// public string Key => "DebugWebsocketSink"; diff --git a/src/PepperDash.Essentials.Core/Room/Interfaces.cs b/src/PepperDash.Essentials.Core/Room/Interfaces.cs index 5d3c704c..499e66a4 100644 --- a/src/PepperDash.Essentials.Core/Room/Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Room/Interfaces.cs @@ -61,10 +61,10 @@ public interface IRunDirectRouteAction /// /// Runs a direct route from a source to a destination with an optional signal type for routing. /// - /// - /// + /// + /// /// - void RunDirectRoute(string sourceKey, string destinationKey, eRoutingSignalType type = eRoutingSignalType.AudioVideo); + void RunDirectRoute(string source, string destination, eRoutingSignalType type = eRoutingSignalType.AudioVideo); } /// diff --git a/src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs b/src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs index eb575815..4222bb4d 100644 --- a/src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs +++ b/src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs @@ -36,6 +36,7 @@ namespace PepperDash.Essentials.Core.Routing /// This method updates the current source for the specified signal type and notifies any subscribers of the change. /// /// The signal type to update. + /// /// The source device to set as the current source. void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice); diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index fe1a448c..86f01803 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -190,6 +190,11 @@ public class EssentialsWebApi : EssentialsDevice Name = "Get Routing Devices and TieLines", RouteHandler = new GetRoutingDevicesAndTieLinesHandler() }, + new HttpCwsRoute("routingFeedbackSession") + { + Name = "Routing Feedback WebSocket Session", + RouteHandler = new RoutingFeedbackSessionRequestHandler() + }, new HttpCwsRoute("initializationExceptions") { Name = "Get Initialization Exceptions", diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs index c0dee8bb..af4e3d38 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs @@ -15,6 +15,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers; /// public class DebugSessionRequestHandler : WebApiBaseRequestHandler { + private CTimer _portForwardTimeoutTimer; + private readonly object _timerLock = new object(); /// /// Constructor @@ -45,6 +47,7 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); var port = 0; + string csIp = null; if (!Debug.WebsocketSink.IsRunning) { @@ -56,6 +59,40 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); } + // Attempt to get the CS LAN IP and forward the port + try + { + var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( + EthernetAdapterType.EthernetCSAdapter); + csIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); + + if (port > 0) + { + var result = CrestronEthernetHelper.AddPortForwarding( + (ushort)port, (ushort)port, csIp, + CrestronEthernetHelper.ePortMapTransport.TCP); + + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) + { + Debug.LogMessage(LogEventLevel.Warning, "Error forwarding port {0} to CS LAN: {1}", port, result); + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Port {0} forwarded to CS LAN for debug websocket", port); + StartPortForwardTimeout(port, csIp); + } + } + } + catch (ArgumentException) + { + Debug.LogMessage(LogEventLevel.Debug, "This processor does not have a CS LAN adapter; skipping port forwarding"); + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Warning, "Error automatically forwarding debug websocket port to CS LAN: {0}", ex.Message); + } + if (!Debug.WebsocketSink.IsRunning) { context.Response.StatusCode = 500; @@ -67,12 +104,14 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler var url = Debug.WebsocketSink.Url; - object data = new + var data = new { - url = Debug.WebsocketSink.Url + url = Debug.WebsocketSink.Url, + fallbackUrl = csIp != null ? url.Replace(csIp, ip) : null }; Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url); + Debug.LogMessage(LogEventLevel.Information, "Fallback Debug Session URL: {0}", data.fallbackUrl); // Return the port number with the full url of the WS Server var res = JsonConvert.SerializeObject(data); @@ -96,9 +135,42 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler /// protected override void HandlePost(HttpCwsContext context) { + CancelPortForwardTimeout(); + + var port = Debug.WebsocketSink.Port; Task.Run(() => Debug.WebsocketSink.StopServer()); + // Remove port forwarding if CS LAN exists + try + { + var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( + EthernetAdapterType.EthernetCSAdapter); + var csIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); + + var result = CrestronEthernetHelper.RemovePortForwarding( + (ushort)port, (ushort)port, csIp, + CrestronEthernetHelper.ePortMapTransport.TCP); + + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) + { + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for port {0}: {1}", port, result); + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed", port); + } + } + catch (ArgumentException) + { + // No CS LAN adapter + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding: {0}", ex.Message); + } + context.Response.StatusCode = 200; context.Response.StatusDescription = "OK"; context.Response.End(); @@ -106,4 +178,53 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler Debug.LogMessage(LogEventLevel.Information, "Websocket Debug Session Stopped"); } + private void StartPortForwardTimeout(int port, string csIp) + { + lock (_timerLock) + { + _portForwardTimeoutTimer?.Dispose(); + _portForwardTimeoutTimer = new CTimer(_ => + { + if (Debug.WebsocketSink.HasActiveConnections) + { + Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward"); + return; + } + + Debug.LogMessage(LogEventLevel.Information, "No debug websocket connection within 30 seconds; removing port forward for port {0}", port); + + try + { + var result = CrestronEthernetHelper.RemovePortForwarding( + (ushort)port, (ushort)port, csIp, + CrestronEthernetHelper.ePortMapTransport.TCP); + + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) + { + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", result); + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed due to timeout", port); + } + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", ex.Message); + } + }, 30000); + } + } + + /// + /// Cancels the port forward timeout timer if a session is being explicitly stopped. + /// + private void CancelPortForwardTimeout() + { + lock (_timerLock) + { + _portForwardTimeoutTimer?.Dispose(); + _portForwardTimeoutTimer = null; + } + } } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs new file mode 100644 index 00000000..4b4981bb --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs @@ -0,0 +1,233 @@ +using System; +using System.Text; +using Crestron.SimplSharp; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Web.RequestHandlers; +using Serilog.Events; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Handles HTTP requests to start and retrieve the routing feedback WebSocket session URL. +/// GET returns the current URL (starting the server if necessary). +/// POST stops the server. +/// +public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler +{ + private static readonly RoutingFeedbackWebsocket _instance = new RoutingFeedbackWebsocket(); + private CTimer _portForwardTimeoutTimer; + private readonly object _timerLock = new object(); + + /// + /// Gets the singleton RoutingFeedbackWebsocket instance. + /// + public static RoutingFeedbackWebsocket Instance => _instance; + + /// + /// Constructor + /// + public RoutingFeedbackSessionRequestHandler() + : base(true) + { + } + + /// + /// Handles GET requests: starts the routing feedback WebSocket if not running and returns the URL. + /// + protected override void HandleGet(HttpCwsContext context) + { + try + { + var ip = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + + var port = 0; + string csIp = null; + + if (!_instance.IsRunning) + { + Debug.LogMessage(LogEventLevel.Information, "Starting Routing Feedback WS Server"); + port = new Random().Next(65335, 65434); + _instance.StartServerAndSetPort(port); + } + + // Attempt to get the CS LAN IP and forward the port + try + { + var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( + EthernetAdapterType.EthernetCSAdapter); + csIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); + + if (port > 0) + { + var result = CrestronEthernetHelper.AddPortForwarding( + (ushort)port, (ushort)port, csIp, + CrestronEthernetHelper.ePortMapTransport.TCP); + + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) + { + Debug.LogMessage(LogEventLevel.Warning, "Error forwarding port {0} to CS LAN: {1}", port, result); + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Port {0} forwarded to CS LAN for routing feedback websocket", port); + StartPortForwardTimeout(port, csIp); + } + } + } + catch (ArgumentException) + { + Debug.LogMessage(LogEventLevel.Debug, "This processor does not have a CS LAN adapter; skipping port forwarding"); + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Warning, "Error automatically forwarding routing feedback websocket port to CS LAN: {0}", ex.Message); + } + + if (!_instance.IsRunning) + { + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write( + JsonConvert.SerializeObject(new { error = "Failed to start routing feedback WebSocket server." }), + false); + context.Response.End(); + return; + } + + var url = _instance.Url; + + var data = new + { + url, + fallbackUrl = csIp != null ? url.Replace(csIp, ip) : null + }; + + Debug.LogMessage(LogEventLevel.Information, "Routing Feedback Session URL: {0}", url); + Debug.LogMessage(LogEventLevel.Information, "Fallback Routing Feedback Session URL: {0}", data.fallbackUrl); + + var json = JsonConvert.SerializeObject(data); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(json, false); + context.Response.End(); + } + catch (Exception ex) + { + Debug.LogError(ex, "Error handling routing feedback session GET: {message}", ex.Message); + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); + } + } + + /// + /// Handles POST requests: stops the routing feedback WebSocket server. + /// + protected override void HandlePost(HttpCwsContext context) + { + try + { + CancelPortForwardTimeout(); + + var port = _instance.Port; + _instance.StopServer(); + + // Remove port forwarding if CS LAN exists + try + { + var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( + EthernetAdapterType.EthernetCSAdapter); + var csIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); + + var result = CrestronEthernetHelper.RemovePortForwarding( + (ushort)port, (ushort)port, csIp, + CrestronEthernetHelper.ePortMapTransport.TCP); + + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) + { + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for port {0}: {1}", port, result); + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed", port); + } + } + catch (ArgumentException) + { + // No CS LAN adapter + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding: {0}", ex.Message); + } + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.End(); + } + catch (Exception ex) + { + Debug.LogError(ex, "Error handling routing feedback session POST: {message}", ex.Message); + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); + } + } + + private void StartPortForwardTimeout(int port, string csIp) + { + lock (_timerLock) + { + _portForwardTimeoutTimer?.Dispose(); + _portForwardTimeoutTimer = new CTimer(_ => + { + if (_instance.HasActiveConnections) + { + Debug.LogMessage(LogEventLevel.Debug, "Routing feedback websocket has active connections; keeping port forward"); + return; + } + + Debug.LogMessage(LogEventLevel.Information, "No routing feedback websocket connection within 30 seconds; removing port forward for port {0}", port); + + try + { + var result = CrestronEthernetHelper.RemovePortForwarding( + (ushort)port, (ushort)port, csIp, + CrestronEthernetHelper.ePortMapTransport.TCP); + + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) + { + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", result); + } + else + { + Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed due to timeout", port); + } + } + catch (Exception ex) + { + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", ex.Message); + } + }, 30000); + } + } + + private void CancelPortForwardTimeout() + { + lock (_timerLock) + { + _portForwardTimeoutTimer?.Dispose(); + _portForwardTimeoutTimer = null; + } + } +} diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs new file mode 100644 index 00000000..a6766a96 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Timers; +using Crestron.SimplSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using PepperDash.Core; +using Serilog.Events; +using WebSocketSharp; +using WebSocketSharp.Server; + +namespace PepperDash.Essentials.Core.Web; + +/// +/// WebSocket service that broadcasts real-time routing state changes to connected clients. +/// Subscribes to route-changed events on midpoint and sink devices and pushes updates +/// to all connected WebSocket clients. +/// +public class RoutingFeedbackWebsocket : IKeyed +{ + private HttpServer _httpsServer; + private readonly string _path = "/routing/join/"; + private const string _certificateName = "selfCres"; + private const string _certificatePassword = "cres12345"; + private const long DEBOUNCE_MS = 200; + + private static string CertPath => + $"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx"; + + private readonly Dictionary _debounceTimers = new Dictionary(); + + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }; + + /// + public string Key => "RoutingFeedbackWebsocket"; + + /// + /// Gets the port number on which the server is currently running. + /// + public int Port => _httpsServer?.Port ?? 0; + + /// + /// Gets the WebSocket URL for the current server instance. + /// + public string Url + { + get + { + if (_httpsServer == null || !_httpsServer.IsListening) return ""; + var service = _httpsServer.WebSocketServices[_path]; + if (service == null) return ""; + + var ip = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1); + if (string.IsNullOrEmpty(ip)) + ip = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + + return $"wss://{ip}:{_httpsServer.Port}{service.Path}"; + } + } + + /// + /// Gets a value indicating whether the server is currently listening. + /// + public bool IsRunning => _httpsServer?.IsListening ?? false; + + /// + /// Gets a value indicating whether there are active WebSocket connections. + /// + public bool HasActiveConnections + { + get + { + if (_httpsServer == null || !_httpsServer.IsListening) return false; + var service = _httpsServer.WebSocketServices[_path]; + if (service == null) return false; + return service.Sessions.Count > 0; + } + } + + /// + /// Starts the WebSocket server on the specified port and subscribes to routing events. + /// + /// The port to listen on. + public void StartServerAndSetPort(int port) + { + if (IsRunning) + { + Debug.LogMessage(LogEventLevel.Information, "Routing feedback WebSocket already running on port {port}", this, Port); + return; + } + + Debug.LogMessage(LogEventLevel.Information, "Starting Routing Feedback WebSocket on port: {port}", this, port); + + try + { + _httpsServer = new HttpServer(port, true); + + var cert = LoadCert(CertPath, _certificatePassword); + _httpsServer.SslConfiguration.ServerCertificate = cert; + _httpsServer.SslConfiguration.ClientCertificateRequired = false; + _httpsServer.SslConfiguration.CheckCertificateRevocation = false; + _httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12; + _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + + _httpsServer.AddWebSocketService(_path, () => new RoutingFeedbackClient(this)); + _httpsServer.Log.Level = LogLevel.Warn; + _httpsServer.Start(); + + SubscribeToRoutingEvents(); + + Debug.LogMessage(LogEventLevel.Information, "Routing Feedback WebSocket ready at {url}", this, Url); + } + catch (Exception ex) + { + Debug.LogError(ex, "Routing Feedback WebSocket failed to start: {message}", this, ex.Message); + _httpsServer = null; + } + } + + /// + /// Stops the WebSocket server and unsubscribes from routing events. + /// + public void StopServer() + { + UnsubscribeFromRoutingEvents(); + + try + { + if (_httpsServer == null || !_httpsServer.IsListening) + return; + + _httpsServer.Log.Output = (d, s) => { }; + _httpsServer.Stop(); + _httpsServer = null; + } + catch (Exception ex) + { + Debug.LogError(ex, "Routing Feedback WebSocket failed to stop: {message}", this, ex.Message); + } + } + + /// + /// Builds and returns the full current routing state snapshot to send to newly connected clients. + /// + internal string GetSnapshotMessage() + { + var midpointRoutes = new Dictionary>(); + var sinkRoutes = new Dictionary(); + + // Collect midpoint current routes + var midpointDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in midpointDevices) + { + if (device.CurrentRoutes == null || device.CurrentRoutes.Count == 0) + continue; + + midpointRoutes[device.Key] = device.CurrentRoutes + .Where(r => r.InputPort != null) + .Select(r => new MidpointRouteDto + { + InputPortKey = r.InputPort.Key, + OutputPortKey = r.OutputPort?.Key, + SignalType = r.InputPort.Type.ToString() + }) + .ToList(); + } + + // Collect sink current sources + var sinkDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in sinkDevices) + { + if (device.CurrentInputPort == null) + continue; + + // Trace back to find source + var tieLine = TieLineCollection.Default.FirstOrDefault(tl => + tl.DestinationPort.Key == device.CurrentInputPort.Key && + tl.DestinationPort.ParentDevice.Key == device.CurrentInputPort.ParentDevice.Key); + + if (tieLine != null) + { + sinkRoutes[device.Key] = new SinkRouteDto + { + InputPortKey = device.CurrentInputPort.Key, + SourceDeviceKey = tieLine.SourcePort.ParentDevice.Key, + SignalType = device.CurrentInputPort.Type.ToString() + }; + } + } + + var snapshot = new RoutingSnapshotDto + { + Type = "snapshot", + MidpointRoutes = midpointRoutes, + SinkRoutes = sinkRoutes + }; + + return JsonConvert.SerializeObject(snapshot, JsonSettings); + } + + private void SubscribeToRoutingEvents() + { + var midpointDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in midpointDevices) + { + device.RouteChanged += HandleMidpointRouteChanged; + } + + var sinkDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in sinkDevices) + { + device.InputChanged += HandleSinkInputChanged; + } + } + + private void UnsubscribeFromRoutingEvents() + { + var midpointDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in midpointDevices) + { + device.RouteChanged -= HandleMidpointRouteChanged; + } + + var sinkDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in sinkDevices) + { + device.InputChanged -= HandleSinkInputChanged; + } + } + + private void HandleMidpointRouteChanged(IRoutingMidpointWithFeedback midpoint, RouteSwitchDescriptor newRoute) + { + DebounceBroadcast($"midpoint-{midpoint.Key}", () => + { + var routes = midpoint.CurrentRoutes? + .Where(r => r.InputPort != null) + .Select(r => new MidpointRouteDto + { + InputPortKey = r.InputPort.Key, + OutputPortKey = r.OutputPort?.Key, + SignalType = r.InputPort.Type.ToString() + }) + .ToList() ?? new List(); + + var msg = new MidpointRouteChangedDto + { + Type = "midpointRouteChanged", + DeviceKey = midpoint.Key, + Routes = routes + }; + + Broadcast(JsonConvert.SerializeObject(msg, JsonSettings)); + }); + } + + private void HandleSinkInputChanged(IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort) + { + DebounceBroadcast($"sink-{sender.Key}", () => + { + var sourceDeviceKey = ""; + if (currentInputPort != null) + { + var tieLine = TieLineCollection.Default.FirstOrDefault(tl => + tl.DestinationPort.Key == currentInputPort.Key && + tl.DestinationPort.ParentDevice.Key == currentInputPort.ParentDevice.Key); + sourceDeviceKey = tieLine?.SourcePort.ParentDevice.Key ?? ""; + } + + var msg = new SinkInputChangedDto + { + Type = "sinkInputChanged", + DeviceKey = sender.Key, + InputPortKey = currentInputPort?.Key ?? "", + SourceDeviceKey = sourceDeviceKey, + SignalType = currentInputPort?.Type.ToString() ?? "" + }; + + Broadcast(JsonConvert.SerializeObject(msg, JsonSettings)); + }); + } + + private void DebounceBroadcast(string key, Action action) + { + lock (_debounceTimers) + { + if (_debounceTimers.TryGetValue(key, out var existingTimer)) + { + existingTimer.Stop(); + existingTimer.Dispose(); + } + + var timer = new Timer(DEBOUNCE_MS) { AutoReset = false }; + timer.Elapsed += (s, e) => + { + try + { + action(); + } + catch (Exception ex) + { + Debug.LogError(ex, "Error in debounced routing broadcast for {key}: {message}", this, key, ex.Message); + } + finally + { + lock (_debounceTimers) + { + if (_debounceTimers.ContainsKey(key)) + { + _debounceTimers[key]?.Dispose(); + _debounceTimers.Remove(key); + } + } + } + }; + timer.Start(); + _debounceTimers[key] = timer; + } + } + + private void Broadcast(string message) + { + if (_httpsServer == null || !_httpsServer.IsListening) return; + + var service = _httpsServer.WebSocketServices[_path]; + if (service == null) return; + + service.Sessions.Broadcast(message); + } + + private static X509Certificate2 LoadCert(string certPath, string certPassword) + { + return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); + } + + // ── DTOs ───────────────────────────────────────────────────────────────── + + private class RoutingSnapshotDto + { + public string Type { get; set; } + public Dictionary> MidpointRoutes { get; set; } + public Dictionary SinkRoutes { get; set; } + } + + private class MidpointRouteChangedDto + { + public string Type { get; set; } + public string DeviceKey { get; set; } + public List Routes { get; set; } + } + + private class SinkInputChangedDto + { + public string Type { get; set; } + public string DeviceKey { get; set; } + public string InputPortKey { get; set; } + public string SourceDeviceKey { get; set; } + public string SignalType { get; set; } + } + + private class MidpointRouteDto + { + public string InputPortKey { get; set; } + public string OutputPortKey { get; set; } + public string SignalType { get; set; } + } + + private class SinkRouteDto + { + public string InputPortKey { get; set; } + public string SourceDeviceKey { get; set; } + public string SignalType { get; set; } + } +} + +/// +/// WebSocket client behavior for routing feedback connections. +/// Sends a full state snapshot on connect. +/// +public class RoutingFeedbackClient : WebSocketBehavior +{ + private readonly RoutingFeedbackWebsocket _owner; + + /// + /// Initializes a new instance of the class. + /// + /// The owning instance. + public RoutingFeedbackClient(RoutingFeedbackWebsocket owner) + { + _owner = owner; + } + + /// + protected override void OnOpen() + { + base.OnOpen(); + Debug.LogMessage(LogEventLevel.Information, "Routing feedback client connected from: {url}", _owner, Context.WebSocket.Url); + + // Send full state snapshot to the newly connected client + try + { + var snapshot = _owner.GetSnapshotMessage(); + Send(snapshot); + } + catch (Exception ex) + { + Debug.LogError(ex, "Error sending routing snapshot to client: {message}", _owner, ex.Message); + } + } + + /// + protected override void OnClose(CloseEventArgs e) + { + base.OnClose(e); + Debug.LogMessage(LogEventLevel.Debug, "Routing feedback client disconnected: {code} {reason}", _owner, e.Code, e.Reason); + } + + /// + protected override void OnError(WebSocketSharp.ErrorEventArgs e) + { + base.OnError(e); + Debug.LogError(e.Exception, "Routing feedback client error: {message}", _owner, e.Message); + } +} From a1d168c4ed997110cd5cc06c94ea0ae0244ae446 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 22 Jun 2026 22:48:16 -0600 Subject: [PATCH 097/161] feat: add HTTP GET handler to RoutingFeedbackWebSocket for client certificate confirmation --- .../Routing/Extensions.cs | 42 +++++++++---------- .../Web/RoutingFeedbackWebsocket.cs | 20 +++++++++ 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 19982f52..bb17eb78 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -50,11 +50,11 @@ public static class Extensions /// private static Dictionary> _tieLinesBySource; - /// - /// Cache of failed route attempts to avoid re-checking impossible paths. - /// Format: "sourceKey|destKey|signalType" - /// - private static readonly HashSet _impossibleRoutes = new HashSet(); + // /// + // /// Cache of failed route attempts to avoid re-checking impossible paths. + // /// Format: "sourceKey|destKey|signalType" + // /// + // private static readonly HashSet _impossibleRoutes = new HashSet(); /// /// Indexes all TieLines by source and destination device keys for faster lookups. @@ -130,14 +130,14 @@ public static class Extensions return string.Format("{0}|{1}|{2}", sourceKey, destKey, type); } - /// - /// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime. - /// - public static void ClearImpossibleRoutesCache() - { - _impossibleRoutes.Clear(); - Debug.LogMessage(LogEventLevel.Information, "Impossible routes cache cleared"); - } + // /// + // /// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime. + // /// + // public static void ClearImpossibleRoutesCache() + // { + // _impossibleRoutes.Clear(); + // Debug.LogMessage(LogEventLevel.Information, "Impossible routes cache cleared"); + // } /// @@ -549,12 +549,12 @@ public static class Extensions { cycle++; - var routeKey = GetRouteKey(source.Key, destination.Key, signalType); - if (_impossibleRoutes.Contains(routeKey)) - { - Debug.LogMessage(LogEventLevel.Verbose, "Route {0} is cached as impossible, skipping", null, routeKey); - return false; - } + // var routeKey = GetRouteKey(source.Key, destination.Key, signalType); + // if (_impossibleRoutes.Contains(routeKey)) + // { + // Debug.LogMessage(LogEventLevel.Verbose, "Route {0} is cached as impossible, skipping", null, routeKey); + // return false; + // } Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString()); @@ -651,8 +651,8 @@ public static class Extensions { Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key); - // Cache this as an impossible route - _impossibleRoutes.Add(routeKey); + // // Cache this as an impossible route + // _impossibleRoutes.Add(routeKey); return false; } diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs index a6766a96..32e2eebf 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Timers; using Crestron.SimplSharp; using Newtonsoft.Json; @@ -11,6 +12,7 @@ using Newtonsoft.Json.Serialization; using PepperDash.Core; using Serilog.Events; using WebSocketSharp; +using WebSocketSharp.Net; using WebSocketSharp.Server; namespace PepperDash.Essentials.Core.Web; @@ -113,6 +115,7 @@ public class RoutingFeedbackWebsocket : IKeyed _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; _httpsServer.AddWebSocketService(_path, () => new RoutingFeedbackClient(this)); + _httpsServer.OnGet += HandleHttpGet; _httpsServer.Log.Level = LogLevel.Warn; _httpsServer.Start(); @@ -337,6 +340,23 @@ public class RoutingFeedbackWebsocket : IKeyed service.Sessions.Broadcast(message); } + private void HandleHttpGet(object sender, HttpRequestEventArgs e) + { + var res = e.Response; + res.ContentType = "text/html"; + res.ContentEncoding = Encoding.UTF8; + res.StatusCode = 200; + + const string html = @" +Essentials Routing Feedback + +

Certificate Accepted

+

You may close this tab and return to the configuration app.

+"; + + res.WriteContent(Encoding.UTF8.GetBytes(html)); + } + private static X509Certificate2 LoadCert(string certPath, string certPassword) { return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); From b7b4b56cec3e22e73d9e87ab10c86cdc62191bf2 Mon Sep 17 00:00:00 2001 From: equinoy Date: Tue, 23 Jun 2026 15:32:16 -0500 Subject: [PATCH 098/161] fix(mobile-control): serve correct MIME types for app static assets Use query-stripped file path for extension detection Add content types for svg, png, jpg/jpeg, gif, webp, and ico Keep existing html/js/css/json handling unchanged --- .../MobileControlWebsocketServer.cs | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 0334df39..6e7a1f5b 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -1354,27 +1354,48 @@ namespace PepperDash.Essentials.WebSocketServer } } - // Set ContentType based on file type + // Set ContentType based on file type. Use filePath so query params do not break extension checks. if (filePath.EndsWith(".html")) { this.LogVerbose("Client requesting User App"); res.ContentType = "text/html"; } - else + else if (filePath.EndsWith(".js")) { - if (path.EndsWith(".js")) - { - res.ContentType = "application/javascript"; - } - else if (path.EndsWith(".css")) - { - res.ContentType = "text/css"; - } - else if (path.EndsWith(".json")) - { - res.ContentType = "application/json"; - } + res.ContentType = "application/javascript"; + } + else if (filePath.EndsWith(".css")) + { + res.ContentType = "text/css"; + } + else if (filePath.EndsWith(".json")) + { + res.ContentType = "application/json"; + } + else if (filePath.EndsWith(".svg")) + { + res.ContentType = "image/svg+xml"; + } + else if (filePath.EndsWith(".png")) + { + res.ContentType = "image/png"; + } + else if (filePath.EndsWith(".jpg") || filePath.EndsWith(".jpeg")) + { + res.ContentType = "image/jpeg"; + } + else if (filePath.EndsWith(".gif")) + { + res.ContentType = "image/gif"; + } + else if (filePath.EndsWith(".webp")) + { + res.ContentType = "image/webp"; + } + else if (filePath.EndsWith(".ico")) + { + res.ContentType = "image/x-icon"; } this.LogVerbose("Attempting to serve file: {filePath}", filePath); From a18f7800d17c07879a237181761d3176e04079a0 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 23 Jun 2026 15:33:46 -0600 Subject: [PATCH 099/161] fix: improve port forwarding logic and error handling in RoutingFeedback session --- .../DebugSessionRequestHandler.cs | 7 +- .../RoutingFeedbackSessionRequestHandler.cs | 110 ++++++++---------- .../Web/RoutingFeedbackWebsocket.cs | 43 +++---- 3 files changed, 70 insertions(+), 90 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs index af4e3d38..c6303190 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs @@ -58,8 +58,12 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler Debug.WebsocketSink.StartServerAndSetPort(port); Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); } + else + { + port = Debug.WebsocketSink.Port; + } - // Attempt to get the CS LAN IP and forward the port + // Always ensure port forwarding is active — it may have been removed by timeout try { var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( @@ -188,6 +192,7 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler if (Debug.WebsocketSink.HasActiveConnections) { Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward"); + StartPortForwardTimeout(port, csIp); return; } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs index 4b4981bb..2ca432c3 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs @@ -10,9 +10,9 @@ using Serilog.Events; namespace PepperDash.Essentials.Core.Web.RequestHandlers; /// -/// Handles HTTP requests to start and retrieve the routing feedback WebSocket session URL. -/// GET returns the current URL (starting the server if necessary). -/// POST stops the server. +/// Handles HTTP requests to start and stop the routing feedback WebSocket session. +/// GET starts the server and returns connection URLs. POST stops the session. +/// Automatically configures port forwarding for Crestron processors with a CS LAN adapter. /// public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler { @@ -20,11 +20,6 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler private CTimer _portForwardTimeoutTimer; private readonly object _timerLock = new object(); - /// - /// Gets the singleton RoutingFeedbackWebsocket instance. - /// - public static RoutingFeedbackWebsocket Instance => _instance; - /// /// Constructor /// @@ -34,7 +29,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler } /// - /// Handles GET requests: starts the routing feedback WebSocket if not running and returns the URL. + /// Starts the routing feedback WebSocket server and returns the connection URL. /// protected override void HandleGet(HttpCwsContext context) { @@ -44,7 +39,6 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); var port = 0; - string csIp = null; if (!_instance.IsRunning) { @@ -52,8 +46,13 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler port = new Random().Next(65335, 65434); _instance.StartServerAndSetPort(port); } + else + { + port = _instance.Port; + } - // Attempt to get the CS LAN IP and forward the port + // Always ensure port forwarding is active — it may have been removed by timeout + string csIp = null; try { var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( @@ -91,10 +90,8 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler { context.Response.StatusCode = 500; context.Response.StatusDescription = "Internal Server Error"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = Encoding.UTF8; context.Response.Write( - JsonConvert.SerializeObject(new { error = "Failed to start routing feedback WebSocket server." }), + JsonConvert.SerializeObject(new { error = "Failed to start routing feedback WebSocket server. Check logs for details." }), false); context.Response.End(); return; @@ -109,20 +106,21 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler }; Debug.LogMessage(LogEventLevel.Information, "Routing Feedback Session URL: {0}", url); - Debug.LogMessage(LogEventLevel.Information, "Fallback Routing Feedback Session URL: {0}", data.fallbackUrl); + if (data.fallbackUrl != null) + Debug.LogMessage(LogEventLevel.Information, "Routing Feedback Fallback URL: {0}", data.fallbackUrl); - var json = JsonConvert.SerializeObject(data); + var res = JsonConvert.SerializeObject(data); - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; context.Response.ContentType = "application/json"; context.Response.ContentEncoding = Encoding.UTF8; - context.Response.Write(json, false); + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.Write(res, false); context.Response.End(); } - catch (Exception ex) + catch (Exception e) { - Debug.LogError(ex, "Error handling routing feedback session GET: {message}", ex.Message); + Debug.LogMessage(LogEventLevel.Error, "Error handling routing feedback session request: {0}", e); context.Response.StatusCode = 500; context.Response.StatusDescription = "Internal Server Error"; context.Response.End(); @@ -130,58 +128,51 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler } /// - /// Handles POST requests: stops the routing feedback WebSocket server. + /// Stops the routing feedback WebSocket session and removes port forwarding. /// protected override void HandlePost(HttpCwsContext context) { + CancelPortForwardTimeout(); + + var port = _instance.Port; + + _instance.StopServer(); + + // Remove port forwarding if CS LAN exists try { - CancelPortForwardTimeout(); + var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( + EthernetAdapterType.EthernetCSAdapter); + var csIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); - var port = _instance.Port; - _instance.StopServer(); + var result = CrestronEthernetHelper.RemovePortForwarding( + (ushort)port, (ushort)port, csIp, + CrestronEthernetHelper.ePortMapTransport.TCP); - // Remove port forwarding if CS LAN exists - try + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) { - var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType( - EthernetAdapterType.EthernetCSAdapter); - var csIp = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); - - var result = CrestronEthernetHelper.RemovePortForwarding( - (ushort)port, (ushort)port, csIp, - CrestronEthernetHelper.ePortMapTransport.TCP); - - if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) - { - Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for port {0}: {1}", port, result); - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed", port); - } + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for routing port {0}: {1}", port, result); } - catch (ArgumentException) + else { - // No CS LAN adapter + Debug.LogMessage(LogEventLevel.Information, "Port forwarding for routing port {0} removed", port); } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding: {0}", ex.Message); - } - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.End(); + } + catch (ArgumentException) + { + // No CS LAN adapter } catch (Exception ex) { - Debug.LogError(ex, "Error handling routing feedback session POST: {message}", ex.Message); - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); + Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for routing: {0}", ex.Message); } + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.End(); + + Debug.LogMessage(LogEventLevel.Information, "Routing Feedback WebSocket Session Stopped"); } private void StartPortForwardTimeout(int port, string csIp) @@ -194,6 +185,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler if (_instance.HasActiveConnections) { Debug.LogMessage(LogEventLevel.Debug, "Routing feedback websocket has active connections; keeping port forward"); + StartPortForwardTimeout(port, csIp); return; } @@ -211,7 +203,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler } else { - Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed due to timeout", port); + Debug.LogMessage(LogEventLevel.Information, "Port forwarding for routing port {0} removed due to timeout", port); } } catch (Exception ex) diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs index 32e2eebf..14139323 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Timers; using Crestron.SimplSharp; using Newtonsoft.Json; @@ -12,7 +11,6 @@ using Newtonsoft.Json.Serialization; using PepperDash.Core; using Serilog.Events; using WebSocketSharp; -using WebSocketSharp.Net; using WebSocketSharp.Server; namespace PepperDash.Essentials.Core.Web; @@ -114,11 +112,11 @@ public class RoutingFeedbackWebsocket : IKeyed _httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12; _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; - _httpsServer.AddWebSocketService(_path, () => new RoutingFeedbackClient(this)); - _httpsServer.OnGet += HandleHttpGet; + _httpsServer.AddWebSocketService(_path); _httpsServer.Log.Level = LogLevel.Warn; _httpsServer.Start(); + RoutingFeedbackClient.Owner = this; SubscribeToRoutingEvents(); Debug.LogMessage(LogEventLevel.Information, "Routing Feedback WebSocket ready at {url}", this, Url); @@ -340,23 +338,6 @@ public class RoutingFeedbackWebsocket : IKeyed service.Sessions.Broadcast(message); } - private void HandleHttpGet(object sender, HttpRequestEventArgs e) - { - var res = e.Response; - res.ContentType = "text/html"; - res.ContentEncoding = Encoding.UTF8; - res.StatusCode = 200; - - const string html = @" -Essentials Routing Feedback - -

Certificate Accepted

-

You may close this tab and return to the configuration app.

-"; - - res.WriteContent(Encoding.UTF8.GetBytes(html)); - } - private static X509Certificate2 LoadCert(string certPath, string certPassword) { return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); @@ -408,32 +389,34 @@ public class RoutingFeedbackWebsocket : IKeyed ///
public class RoutingFeedbackClient : WebSocketBehavior { - private readonly RoutingFeedbackWebsocket _owner; + /// + /// Static reference to the owning instance. + /// Set before the server starts accepting connections. + /// + internal static RoutingFeedbackWebsocket Owner { get; set; } /// /// Initializes a new instance of the class. /// - /// The owning instance. - public RoutingFeedbackClient(RoutingFeedbackWebsocket owner) + public RoutingFeedbackClient() { - _owner = owner; } /// protected override void OnOpen() { base.OnOpen(); - Debug.LogMessage(LogEventLevel.Information, "Routing feedback client connected from: {url}", _owner, Context.WebSocket.Url); + Debug.LogMessage(LogEventLevel.Information, "Routing feedback client connected from: {url}", Owner, Context.WebSocket.Url); // Send full state snapshot to the newly connected client try { - var snapshot = _owner.GetSnapshotMessage(); + var snapshot = Owner.GetSnapshotMessage(); Send(snapshot); } catch (Exception ex) { - Debug.LogError(ex, "Error sending routing snapshot to client: {message}", _owner, ex.Message); + Debug.LogError(ex, "Error sending routing snapshot to client: {message}", Owner, ex.Message); } } @@ -441,13 +424,13 @@ public class RoutingFeedbackClient : WebSocketBehavior protected override void OnClose(CloseEventArgs e) { base.OnClose(e); - Debug.LogMessage(LogEventLevel.Debug, "Routing feedback client disconnected: {code} {reason}", _owner, e.Code, e.Reason); + Debug.LogMessage(LogEventLevel.Debug, "Routing feedback client disconnected: {code} {reason}", Owner, e.Code, e.Reason); } /// protected override void OnError(WebSocketSharp.ErrorEventArgs e) { base.OnError(e); - Debug.LogError(e.Exception, "Routing feedback client error: {message}", _owner, e.Message); + Debug.LogError(e.Exception, "Routing feedback client error: {message}", Owner, e.Message); } } From a732c5e08e37ec5e3d9e241061fd210938fd9ac3 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 23 Jun 2026 16:08:42 -0600 Subject: [PATCH 100/161] feat: enhance WebSocket services with HTTP GET handler for certificate confirmation and increase timeout for port forwarding --- .../Logging/DebugWebsocketSink.cs | 11 ++++++++ .../DebugSessionRequestHandler.cs | 4 +-- .../RoutingFeedbackSessionRequestHandler.cs | 2 +- .../Web/RoutingFeedbackWebsocket.cs | 11 ++++++++ .../VideoCodec/MockVC/MockVC.cs | 28 ++++++++++++++++++- .../VideoCodec/VideoCodecBase.cs | 2 +- 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs index 3027640f..07e8f489 100644 --- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs +++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs @@ -262,6 +262,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed } Debug.LogInformation("Adding Debug Client Service"); _httpsServer.AddWebSocketService(_path); + _httpsServer.OnGet += HandleHttpGet; Debug.LogInformation("Assigning Log Info"); _httpsServer.Log.Level = LogLevel.Trace; _httpsServer.Log.Output = WriteWebSocketInternalLog; @@ -279,6 +280,16 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed } } + private void HandleHttpGet(object sender, HttpRequestEventArgs e) + { + var res = e.Response; + var body = System.Text.Encoding.UTF8.GetBytes( + "

Certificate accepted.

You can close this tab and return to the application.

"); + res.ContentType = "text/html"; + res.ContentLength64 = body.Length; + res.Close(body, true); + } + /// /// Stops the WebSocket server if it is currently running. /// diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs index c6303190..ea9c5b82 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs @@ -196,7 +196,7 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler return; } - Debug.LogMessage(LogEventLevel.Information, "No debug websocket connection within 30 seconds; removing port forward for port {0}", port); + Debug.LogMessage(LogEventLevel.Information, "No debug websocket connection within timeout; removing port forward for port {0}", port); try { @@ -217,7 +217,7 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler { Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", ex.Message); } - }, 30000); + }, 120000); } } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs index 2ca432c3..d095b78f 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RoutingFeedbackSessionRequestHandler.cs @@ -210,7 +210,7 @@ public class RoutingFeedbackSessionRequestHandler : WebApiBaseRequestHandler { Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", ex.Message); } - }, 30000); + }, 120000); } } diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs index 14139323..444a1e8c 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -113,6 +113,7 @@ public class RoutingFeedbackWebsocket : IKeyed _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; _httpsServer.AddWebSocketService(_path); + _httpsServer.OnGet += HandleHttpGet; _httpsServer.Log.Level = LogLevel.Warn; _httpsServer.Start(); @@ -338,6 +339,16 @@ public class RoutingFeedbackWebsocket : IKeyed service.Sessions.Broadcast(message); } + private void HandleHttpGet(object sender, HttpRequestEventArgs e) + { + var res = e.Response; + var body = System.Text.Encoding.UTF8.GetBytes( + "

Certificate accepted.

You can close this tab and return to the application.

"); + res.ContentType = "text/html"; + res.ContentLength64 = body.Length; + res.Close(body, true); + } + private static X509Certificate2 LoadCert(string certPath, string certPassword) { return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet); diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs index 7b8f73a3..56a4106e 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs @@ -21,7 +21,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// /// Represents a MockVC /// - public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets + public class MockVC : VideoCodecBase, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingSinkWithFeedback { /// /// Gets or sets the PropertiesConfig @@ -858,6 +858,32 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec #endregion + #region IRoutingSinkWithFeedback Members + + public RoutingInputPort CurrentInputPort { get; private set; } + + public event InputChangedEventHandler InputChanged; + + #endregion + + #region ICurrentSources Members + + public Dictionary CurrentSources { get; private set; } = new Dictionary(); + + public Dictionary CurrentSourceKeys { get; private set; } = new Dictionary(); + + public event EventHandler CurrentSourcesChanged; + + public void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice) + { + CurrentSources.TryGetValue(signalType, out var previousSource); + CurrentSources[signalType] = sourceDevice; + CurrentSourceKeys[signalType] = sourceDevice?.Key; + CurrentSourcesChanged?.Invoke(this, new CurrentSourcesChangedEventArgs(signalType, previousSource, sourceDevice)); + } + + #endregion + /// protected override void CustomSetConfig(DeviceConfig config) { diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index de6900af..98a44b40 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -27,7 +27,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec; /// Base class for video codecs. Contains common properties, methods, and feedback for video codecs. /// Also contains the logic to link commonly implemented interfaces to the API bridge. /// -public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingMidpoint, +public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingSource, IUsageTracking, ICodecCallControls, IHasContentSharing, ICodecAudio, IVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode, IHasReady { private const int XSigEncoding = 28591; From f4fe8eff906db006ddf341f8136c709ec194cf50 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 24 Jun 2026 20:10:52 -0600 Subject: [PATCH 101/161] feat: enhance routing and messaging logic with improved source handling and batch processing capabilities --- .../Routing/Extensions.cs | 39 ++++++++ .../Audio/GenericAudioOut.cs | 17 ++-- .../Displays/DisplayBase.cs | 17 ++-- .../Generic/GenericSink.cs | 17 ++-- .../SoftCodec/BlueJeansPc.cs | 17 ++-- .../SoftCodec/GenericSoftCodec.cs | 17 ++-- .../Messengers/ICurrentSourcesMessenger.cs | 9 -- .../MessageToClients.cs | 39 ++++---- .../MobileControlSystemController.cs | 90 ++++++++++++++++++- .../TransmitMessage.cs | 28 +++--- 10 files changed, 214 insertions(+), 76 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index bb17eb78..1e31127d 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -411,6 +411,20 @@ public static class Extensions audioOrSingleRoute.ExecuteRoutes(); videoRoute?.ExecuteRoutes(); + + // Update ICurrentSources on the destination if it implements the interface + if (request.Destination is ICurrentSources currentSourcesDevice && request.Source is IRoutingSource routingSource) + { + if (request.SignalType.HasFlag(eRoutingSignalType.Audio) || request.SignalType.HasFlag(eRoutingSignalType.AudioVideo)) + { + currentSourcesDevice.SetCurrentSource(eRoutingSignalType.Audio, routingSource); + } + + if (request.SignalType.HasFlag(eRoutingSignalType.Video) || request.SignalType.HasFlag(eRoutingSignalType.AudioVideo)) + { + currentSourcesDevice.SetCurrentSource(eRoutingSignalType.Video, routingSource); + } + } } catch (Exception ex) { @@ -445,6 +459,31 @@ public static class Extensions Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key); current.ReleaseRoutes(clearRoute); } + + // Clear ICurrentSources on the destination if clearing the route + if (clearRoute && destination is ICurrentSources currentSourcesDevice) + { + if (current != null) + { + var signalType = current.SignalType; + + if (signalType.HasFlag(eRoutingSignalType.Audio) || signalType.HasFlag(eRoutingSignalType.AudioVideo)) + { + currentSourcesDevice.SetCurrentSource(eRoutingSignalType.Audio, null); + } + + if (signalType.HasFlag(eRoutingSignalType.Video) || signalType.HasFlag(eRoutingSignalType.AudioVideo)) + { + currentSourcesDevice.SetCurrentSource(eRoutingSignalType.Video, null); + } + } + else + { + // No route descriptor found, clear all signal types + currentSourcesDevice.SetCurrentSource(eRoutingSignalType.Audio, null); + currentSourcesDevice.SetCurrentSource(eRoutingSignalType.Video, null); + } + } } catch (Exception ex) { diff --git a/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs b/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs index 24df64ae..d9106489 100644 --- a/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs +++ b/src/PepperDash.Essentials.Devices.Common/Audio/GenericAudioOut.cs @@ -74,14 +74,17 @@ public class GenericAudioOut : EssentialsDevice, IRoutingSinkWithFeedback continue; } + if (!signalType.HasFlag(type)) + { + this.LogDebug("Skipping {type}", type); + continue; + } + this.LogDebug("setting {type}", type); - var previousSource = CurrentSources[type]; + CurrentSources.TryGetValue(type, out var previousSource); - if (signalType.HasFlag(type)) - { - UpdateCurrentSources(type, previousSource, sourceDevice); - } + UpdateCurrentSources(type, previousSource, sourceDevice); } } @@ -99,11 +102,11 @@ public class GenericAudioOut : EssentialsDevice, IRoutingSinkWithFeedback // Update the current source key for the specified signal type if (CurrentSourceKeys.ContainsKey(signalType)) { - CurrentSourceKeys[signalType] = sourceDevice.Key; + CurrentSourceKeys[signalType] = sourceDevice?.Key; } else { - CurrentSourceKeys.Add(signalType, sourceDevice.Key); + CurrentSourceKeys.Add(signalType, sourceDevice?.Key); } // Raise the CurrentSourcesChanged event diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs index 89f6f415..6662ee75 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs @@ -373,14 +373,17 @@ public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, continue; } + if (!signalType.HasFlag(type)) + { + this.LogDebug("Skipping {type}", type); + continue; + } + this.LogDebug("setting {type}", type); - var previousSource = CurrentSources[type]; + CurrentSources.TryGetValue(type, out var previousSource); - if (signalType.HasFlag(type)) - { - UpdateCurrentSources(type, previousSource, sourceDevice); - } + UpdateCurrentSources(type, previousSource, sourceDevice); } } @@ -398,11 +401,11 @@ public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources, // Update the current source key for the specified signal type if (CurrentSourceKeys.ContainsKey(signalType)) { - CurrentSourceKeys[signalType] = sourceDevice.Key; + CurrentSourceKeys[signalType] = sourceDevice?.Key; } else { - CurrentSourceKeys.Add(signalType, sourceDevice.Key); + CurrentSourceKeys.Add(signalType, sourceDevice?.Key); } // Raise the CurrentSourcesChanged event diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index be3590ad..4b54cf2a 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -63,14 +63,17 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithFeedback continue; } + if (!signalType.HasFlag(type)) + { + this.LogDebug("Skipping {type}", type); + continue; + } + this.LogDebug("setting {type}", type); - var previousSource = CurrentSources[type]; + CurrentSources.TryGetValue(type, out var previousSource); - if (signalType.HasFlag(type)) - { - UpdateCurrentSources(type, previousSource, sourceDevice); - } + UpdateCurrentSources(type, previousSource, sourceDevice); } } @@ -88,11 +91,11 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithFeedback // Update the current source key for the specified signal type if (CurrentSourceKeys.ContainsKey(signalType)) { - CurrentSourceKeys[signalType] = sourceDevice.Key; + CurrentSourceKeys[signalType] = sourceDevice?.Key; } else { - CurrentSourceKeys.Add(signalType, sourceDevice.Key); + CurrentSourceKeys.Add(signalType, sourceDevice?.Key); } // Raise the CurrentSourcesChanged event diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs index f2ab5254..c27502d4 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/BlueJeansPc.cs @@ -93,14 +93,17 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSinkWithFeedback continue; } + if (!signalType.HasFlag(type)) + { + this.LogDebug("Skipping {type}", type); + continue; + } + this.LogDebug("setting {type}", type); - var previousSource = CurrentSources[type]; + CurrentSources.TryGetValue(type, out var previousSource); - if (signalType.HasFlag(type)) - { - UpdateCurrentSources(type, previousSource, sourceDevice); - } + UpdateCurrentSources(type, previousSource, sourceDevice); } } @@ -118,11 +121,11 @@ public class BlueJeansPc : InRoomPc, IRunRouteAction, IRoutingSinkWithFeedback // Update the current source key for the specified signal type if (CurrentSourceKeys.ContainsKey(signalType)) { - CurrentSourceKeys[signalType] = sourceDevice.Key; + CurrentSourceKeys[signalType] = sourceDevice?.Key; } else { - CurrentSourceKeys.Add(signalType, sourceDevice.Key); + CurrentSourceKeys.Add(signalType, sourceDevice?.Key); } // Raise the CurrentSourcesChanged event diff --git a/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs b/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs index 3e40f6b6..e13e3804 100644 --- a/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs +++ b/src/PepperDash.Essentials.Devices.Common/SoftCodec/GenericSoftCodec.cs @@ -100,14 +100,17 @@ public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWi continue; } + if (!signalType.HasFlag(type)) + { + this.LogDebug("Skipping {type}", type); + continue; + } + this.LogDebug("setting {type}", type); - var previousSource = CurrentSources[type]; + CurrentSources.TryGetValue(type, out var previousSource); - if (signalType.HasFlag(type)) - { - UpdateCurrentSources(type, previousSource, sourceDevice); - } + UpdateCurrentSources(type, previousSource, sourceDevice); } } @@ -125,11 +128,11 @@ public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWi // Update the current source key for the specified signal type if (CurrentSourceKeys.ContainsKey(signalType)) { - CurrentSourceKeys[signalType] = sourceDevice.Key; + CurrentSourceKeys[signalType] = sourceDevice?.Key; } else { - CurrentSourceKeys.Add(signalType, sourceDevice.Key); + CurrentSourceKeys.Add(signalType, sourceDevice?.Key); } // Raise the CurrentSourcesChanged event diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs index ae00d78a..c8de380c 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs @@ -42,12 +42,10 @@ namespace PepperDash.Essentials.AppServer.Messengers { // need to copy the dictionaries to avoid enumeration issues var currentSourceKeys = sourceDevice.CurrentSourceKeys.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var currentSources = sourceDevice.CurrentSources.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); PostStatusMessage(JToken.FromObject(new { currentSourceKeys, - currentSources, })); }; } @@ -57,7 +55,6 @@ namespace PepperDash.Essentials.AppServer.Messengers var message = new CurrentSourcesStateMessage { CurrentSourceKeys = sourceDevice.CurrentSourceKeys, - CurrentSources = sourceDevice.CurrentSources }; PostStatusMessage(message, id); @@ -75,11 +72,5 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("currentSourceKeys", NullValueHandling = NullValueHandling.Ignore)] public Dictionary CurrentSourceKeys { get; set; } - - /// - /// Gets or sets the CurrentSource - /// - [JsonProperty("currentSources")] - public Dictionary CurrentSources { get; set; } } } diff --git a/src/PepperDash.Essentials.MobileControl/MessageToClients.cs b/src/PepperDash.Essentials.MobileControl/MessageToClients.cs index e1aa5eb5..34924cf9 100644 --- a/src/PepperDash.Essentials.MobileControl/MessageToClients.cs +++ b/src/PepperDash.Essentials.MobileControl/MessageToClients.cs @@ -16,35 +16,46 @@ namespace PepperDash.Essentials ///
public class MessageToClients : IQueueMessage { + private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + Converters = { new IsoDateTimeConverter() } + }; + private readonly MobileControlWebsocketServer _server; - private readonly object msgToSend; + private readonly string _serializedMessage; + private readonly string _clientId; /// - /// Message to send to Direct Server Clients + /// Message to send to Direct Server Clients. + /// Serialization occurs here in the caller's thread context (parallel) rather than on the queue thread (sequential). /// /// message object to send /// WebSocket server instance public MessageToClients(object msg, MobileControlWebsocketServer server) { _server = server; - msgToSend = msg; + _serializedMessage = JsonConvert.SerializeObject(msg, Formatting.None, SerializerSettings); + _clientId = (msg as MobileControlMessage)?.ClientId; } /// - /// Message to send to Direct Server Clients + /// Message to send to Direct Server Clients. + /// Serialization occurs here in the caller's thread context (parallel) rather than on the queue thread (sequential). /// /// message object to send /// WebSocket server instance public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server) { _server = server; - msgToSend = msg; + _serializedMessage = JsonConvert.SerializeObject(msg, Formatting.None, SerializerSettings); + _clientId = null; } #region Implementation of IQueueMessage /// - /// Dispatch method + /// Dispatch method - only handles WebSocket send since serialization was done at construction time /// public void Dispatch() { @@ -56,24 +67,18 @@ namespace PepperDash.Essentials 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) + if (_clientId != null) { - var clientId = clientSpecificMessage.ClientId; + _server.LogVerbose("Message TX To client {clientId}: {message}", _clientId, _serializedMessage); - _server.LogVerbose("Message TX To client {clientId}: {message}", clientId, message); - - _server.SendMessageToClient(clientId, message); + _server.SendMessageToClient(_clientId, _serializedMessage); return; } - _server.SendMessageToAllClients(message); + _server.SendMessageToAllClients(_serializedMessage); - _server.LogVerbose("Message TX To all clients: {message}", message); + _server.LogVerbose("Message TX To all clients: {message}", _serializedMessage); } catch (ThreadAbortException) { diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 683877fd..e81c53d4 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -196,14 +196,14 @@ namespace PepperDash.Essentials _receiveQueue = new GenericQueue( key + "-rxqueue", System.Threading.ThreadPriority.Highest, - 25 + 100 ); // The queue that will collect the outgoing messages in the order they are received _transmitToServerQueue = new GenericQueue( key + "-txqueue", System.Threading.ThreadPriority.Highest, - 25 + 100 ); if (Config.DirectServer != null && Config.DirectServer.EnableDirectServer) @@ -218,7 +218,7 @@ namespace PepperDash.Essentials _transmitToClientsQueue = new GenericQueue( key + "-clienttxqueue", System.Threading.ThreadPriority.Highest, - 25 + 100 ); } @@ -1500,6 +1500,87 @@ namespace PepperDash.Essentials }); } + /// + /// Handles a batch request for full status of multiple devices. + /// Triggers all registered messengers for each device key in parallel, + /// then sends an /system/initialSyncComplete message after all have been processed. + /// + private void HandleBatchDeviceFullStatus(string clientId, JToken content) + { + if (content == null) + { + this.LogWarning("BatchDeviceFullStatus: content is null"); + return; + } + + var deviceKeys = content.SelectToken("deviceKeys")?.ToObject>(); + + if (deviceKeys == null || deviceKeys.Count == 0) + { + this.LogWarning("BatchDeviceFullStatus: No device keys provided"); + SendMessageObject(new MobileControlMessage + { + Type = "/system/initialSyncComplete", + ClientId = clientId + }); + return; + } + + this.LogInformation("BatchDeviceFullStatus: Processing {count} device keys", deviceKeys.Count); + + var tasks = new List(); + + foreach (var deviceKey in deviceKeys) + { + var fullStatusPath = $"/device/{deviceKey}/fullStatus"; + + var handlers = _actionDictionary + .Where(kv => fullStatusPath.StartsWith(kv.Key + "/")) + .SelectMany(kv => kv.Value) + .ToList(); + + if (handlers.Count == 0) + { + this.LogDebug("BatchDeviceFullStatus: No handlers for {deviceKey}", deviceKey); + continue; + } + + foreach (var handler in handlers) + { + tasks.Add(Task.Run(() => + { + try + { + handler.Action(fullStatusPath, clientId, JToken.FromObject(new { deviceKey })); + } + catch (Exception ex) + { + this.LogError("BatchDeviceFullStatus: Exception in handler for {deviceKey}: {message}", deviceKey, ex.Message); + } + })); + } + } + + // After all handlers have completed and enqueued their responses, send sync complete + Task.Run(async () => + { + try + { + await Task.WhenAll(tasks); + } + catch (Exception ex) + { + this.LogError("BatchDeviceFullStatus: Exception waiting for tasks: {message}", ex.Message); + } + + SendMessageObject(new MobileControlMessage + { + Type = "/system/initialSyncComplete", + ClientId = clientId + }); + }); + } + private void SendDeviceInterfaces(string clientId) { this.LogDebug("Sending Device interfaces"); @@ -1602,6 +1683,9 @@ namespace PepperDash.Essentials case "/system/clientJoined": HandleClientJoined(message.Content); break; + case "/system/batchDeviceFullStatus": + HandleBatchDeviceFullStatus(message.ClientId, message.Content); + break; case "/system/reboot": SystemMonitorController.ProcessorReboot(); break; diff --git a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs index 06595a9d..e08d0c3c 100644 --- a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs +++ b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs @@ -13,35 +13,43 @@ namespace PepperDash.Essentials ///
public class TransmitMessage : IQueueMessage { + private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + Converters = { new IsoDateTimeConverter() } + }; + private readonly WebSocket _ws; - private readonly object msgToSend; + private readonly string _serializedMessage; /// - /// Initialize a message to send + /// Initialize a message to send. + /// Serialization occurs here in the caller's thread context rather than on the queue thread. /// /// message object to send /// WebSocket instance public TransmitMessage(object msg, WebSocket ws) { _ws = ws; - msgToSend = msg; + _serializedMessage = JsonConvert.SerializeObject(msg, Formatting.None, SerializerSettings); } /// - /// Initialize a message to send + /// Initialize a message to send. + /// Serialization occurs here in the caller's thread context rather than on the queue thread. /// /// message object to send /// WebSocket instance public TransmitMessage(DeviceStateMessageBase msg, WebSocket ws) { _ws = ws; - msgToSend = msg; + _serializedMessage = JsonConvert.SerializeObject(msg, Formatting.None, SerializerSettings); } #region Implementation of IQueueMessage /// - /// Dispatch method + /// Dispatch method - only handles WebSocket send since serialization was done at construction time /// public void Dispatch() { @@ -59,13 +67,9 @@ namespace PepperDash.Essentials return; } + Debug.LogVerbose("Message TX: {0}", _serializedMessage); - var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - - Debug.LogVerbose("Message TX: {0}", message); - - _ws.Send(message); + _ws.Send(_serializedMessage); } catch (Exception ex) { From f7c3ed4b8b2d991f57a1547fdf732d9f548351bf Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 24 Jun 2026 22:28:40 -0600 Subject: [PATCH 102/161] feat: add mobile control messengers for participant audio and video mute functionality --- .../VideoCodec/Interfaces/Participant.cs | 15 ++++++- .../IHasParticipantAudioMuteMessenger.cs | 40 +++++++++++++++++ .../IHasParticipantVideoMuteMessenger.cs | 44 +++++++++++++++++++ .../MessengerFactoryRegistry.cs | 11 +++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasParticipantAudioMuteMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasParticipantVideoMuteMessenger.cs diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs index e8c46f7a..1c8678b3 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces { /// @@ -8,61 +9,73 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces /// /// Gets or sets the UserId /// + [JsonProperty("userId")] public int UserId { get; set; } /// /// Gets or sets the IsHost /// + [JsonProperty("isHost")] public bool IsHost { get; set; } /// /// Gets or sets the IsCohost /// + [JsonProperty("isCohost")] public bool IsCohost { get; set; } /// /// Gets or sets the IsMyself /// + [JsonProperty("isMyself")] public bool IsMyself { get; set; } /// /// Gets or sets the Name /// + [JsonProperty("name")] public string Name { get; set; } /// /// Gets or sets the Email /// + [JsonProperty("email")] public bool CanMuteVideo { get; set; } /// /// Gets or sets the CanUnmuteVideo /// + [JsonProperty("canUnmuteVideo")] public bool CanUnmuteVideo { get; set; } /// /// Gets or sets the CanMuteAudio /// - public bool VideoMuteFb { get; set; } + [JsonProperty("canMuteAudio")] + public bool CanMuteAudio { get; set; } /// /// Gets or sets the AudioMuteFb /// + [JsonProperty("audioMuteFb")] public bool AudioMuteFb { get; set; } /// /// Gets or sets the HandIsRaisedFb /// + [JsonProperty("handIsRaisedFb")] public bool HandIsRaisedFb { get; set; } /// /// Gets or sets the IsPinnedFb /// + [JsonProperty("isPinnedFb")] public bool IsPinnedFb { get; set; } /// /// Gets or sets the ScreenIndexIsPinnedToFb /// + [JsonProperty("screenIndexIsPinnedToFb")] public int ScreenIndexIsPinnedToFb { get; set; } /// diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasParticipantAudioMuteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasParticipantAudioMuteMessenger.cs new file mode 100644 index 00000000..e7f6cebe --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasParticipantAudioMuteMessenger.cs @@ -0,0 +1,40 @@ +using System; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.AppServer; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Mobile Control messenger for : + /// mute-all and per-participant audio/video mute toggles. Action-only (no status of its own). + /// + public class IHasParticipantAudioMuteMessenger : MessengerBase + { + private readonly IHasParticipantAudioMute _codec; + + public IHasParticipantAudioMuteMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _codec = device as IHasParticipantAudioMute ?? throw new ArgumentNullException(nameof(device)); + } + + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/muteAllParticipants", (id, content) => _codec.MuteAudioForAllParticipants()); + AddAction("/toggleParticipantAudioMute", (id, content) => + { + var i = content?.ToObject>(); + if (i != null) _codec.ToggleAudioForParticipant(i.Value); + }); + AddAction("/toggleParticipantVideoMute", (id, content) => + { + var i = content?.ToObject>(); + if (i != null) _codec.ToggleVideoForParticipant(i.Value); + }); + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasParticipantVideoMuteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasParticipantVideoMuteMessenger.cs new file mode 100644 index 00000000..449a51d0 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasParticipantVideoMuteMessenger.cs @@ -0,0 +1,44 @@ +using System; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.AppServer; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Mobile Control messenger for : + /// per-participant video mute/unmute/toggle. Action-only (no status of its own). + /// + public class IHasParticipantVideoMuteMessenger : MessengerBase + { + private readonly IHasParticipantVideoMute _codec; + + public IHasParticipantVideoMuteMessenger(string key, string messagePath, EssentialsDevice device) + : base(key, messagePath, device) + { + _codec = device as IHasParticipantVideoMute ?? throw new ArgumentNullException(nameof(device)); + } + + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/muteVideoForParticipant", (id, content) => + { + var i = content?.ToObject>(); + if (i != null) _codec.MuteVideoForParticipant(i.Value); + }); + AddAction("/unmuteVideoForParticipant", (id, content) => + { + var i = content?.ToObject>(); + if (i != null) _codec.UnmuteVideoForParticipant(i.Value); + }); + AddAction("/toggleParticipantVideoMute", (id, content) => + { + var i = content?.ToObject>(); + if (i != null) _codec.ToggleVideoForParticipant(i.Value); + }); + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index 7124de56..e5c7e19a 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -236,6 +236,17 @@ namespace PepperDash.Essentials (d, mp, ck) => new IHasFarEndContentStatusMessenger( $"{d.Key}-farEndContent-{ck}", mp, d) ), + new MessengerFactoryEntry( + typeof(IHasParticipantAudioMute), + (d, mp, ck) => new IHasParticipantAudioMuteMessenger( + $"{d.Key}-participantAudioMute-{ck}", mp, d) + ), + new MessengerFactoryEntry( + typeof(IHasParticipantVideoMute), + (d, mp, ck) => new IHasParticipantVideoMuteMessenger( + $"{d.Key}-participantVideoMute-{ck}", mp, d), + predicate: d => d is IHasParticipantVideoMute && !(d is IHasParticipantAudioMute) + ), new MessengerFactoryEntry( typeof(IHasMeetingInfo), (d, mp, ck) => new IHasMeetingInfoMessenger( From 77c78af74bddda645a38c2c650c71191383e5510 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 24 Jun 2026 22:29:31 -0600 Subject: [PATCH 103/161] feat: add VideoMuteFb property to Participant class for video mute feedback --- .../VideoCodec/Interfaces/Participant.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs index 1c8678b3..5dfd18a0 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/Interfaces/Participant.cs @@ -60,6 +60,12 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces [JsonProperty("audioMuteFb")] public bool AudioMuteFb { get; set; } + /// + /// Gets or sets the VideoMuteFb + /// + [JsonProperty("videoMuteFb")] + public bool VideoMuteFb { get; set; } + /// /// Gets or sets the HandIsRaisedFb /// From 8748157362b144264e8460edd3e7d15121da5c4d Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 25 Jun 2026 11:46:27 -0600 Subject: [PATCH 104/161] feat: enhance IHasDirectoryMessenger with new actions and state messaging for directory management feat: update MobileControlConfig to enable messenger subscriptions by default fix: improve HandleBatchDeviceFullStatus to support granular device status requests and enhance error handling --- .../Messengers/ICurrentSourcesMessenger.cs | 2 +- .../Messengers/IHasDirectoryMessenger.cs | 21 +++++ .../MobileControlConfig.cs | 5 +- .../MobileControlSystemController.cs | 93 ++++++++++++++----- 4 files changed, 93 insertions(+), 28 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs index c8de380c..12d1dea6 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs @@ -50,7 +50,7 @@ namespace PepperDash.Essentials.AppServer.Messengers }; } - private void SendCurrentSourceStatus(string id = null) + private void SendCurrentSourceStatus(string id) { var message = new CurrentSourcesStateMessage { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs index a19a4ae8..31d61da6 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs @@ -36,12 +36,16 @@ namespace PepperDash.Essentials.AppServer.Messengers AddAction("/getDirectory", (id, content) => GetDirectoryRoot()); + AddAction("/setCurrentDirectoryToRoot", (id, content) => _directory.SetCurrentDirectoryToRoot()); + AddAction("/directoryById", (id, content) => { var msg = content.ToObject>(); GetDirectory(msg.Value); }); + AddAction("/getDirectoryPareentFolderContents", (id, content) => _directory.GetDirectoryParentFolderContents()); + AddAction("/directorySearch", (id, content) => { var msg = content.ToObject>(); @@ -52,6 +56,14 @@ namespace PepperDash.Essentials.AppServer.Messengers _directory.DirectoryResultReturned += DirectoryResultReturned; _directory.PhonebookSyncState.InitialSyncCompleted += PhonebookSyncState_InitialSyncCompleted; + + _directory.CurrentDirectoryResultIsNotDirectoryRoot.OutputChange += (s, e) => + { + PostStatusMessage(new IHasDirectoryStateMessage + { + DirectorySelectedFolderIsNotRoot = _directory.CurrentDirectoryResultIsNotDirectoryRoot?.BoolValue + }); + }; } private void DirectoryResultReturned(object sender, DirectoryEventArgs e) @@ -125,16 +137,22 @@ namespace PepperDash.Essentials.AppServer.Messengers { PostStatusMessage(new IHasDirectoryStateMessage { + DirectoryRoot = _directory.DirectoryRoot, CurrentDirectory = _directory.CurrentDirectoryResult, InitialPhonebookSyncComplete = _directory.PhonebookSyncState.InitialSyncComplete, HasDirectory = true, HasDirectorySearch = true, + DirectorySelectedFolderName = _directory.CurrentDirectoryResult?.CurrentDirectoryResults?.Count > 0 ? _directory.CurrentDirectoryResult.CurrentDirectoryResults[0].Name : null, + DirectorySelectedFolderIsNotRoot = _directory.CurrentDirectorResultIsNotDirectoryRoot?.BoolValue }); } } public class IHasDirectoryStateMessage : DeviceStateMessageBase { + [JsonProperty("directoryRoot", NullValueHandling = NullValueHandling.Ignore)] + public CodecDirectory DirectoryRoot { get; set; } + [JsonProperty("currentDirectory", NullValueHandling = NullValueHandling.Ignore)] public CodecDirectory CurrentDirectory { get; set; } @@ -153,5 +171,8 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("directorySelectedFolderName", NullValueHandling = NullValueHandling.Ignore)] public string DirectorySelectedFolderName { get; set; } + [JsonProperty("directorySelectedFolderIsNotRoot", NullValueHandling = NullValueHandling.Ignore)] + public bool? DirectorySelectedFolderIsNotRoot { get; set; } + } } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs index ec7219a3..b74e30cf 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs @@ -40,10 +40,11 @@ namespace PepperDash.Essentials public bool EnableApiServer { get; set; } = true; /// - /// Enable subscriptions for Messengers + /// Enable subscriptions for Messengers. + /// Defaults to true for v3.x+ /// [JsonProperty("enableMessengerSubscriptions")] - public bool EnableMessengerSubscriptions { get; set; } + public bool EnableMessengerSubscriptions { get; set; } = true; } /// diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index e81c53d4..129af42f 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1501,8 +1501,14 @@ namespace PepperDash.Essentials } /// - /// Handles a batch request for full status of multiple devices. - /// Triggers all registered messengers for each device key in parallel, + /// Handles a batch request for device status. Supports two content formats: + /// + /// Granular format: { "devices": { "deviceKey": ["/fullStatus", "/layoutStatus"], ... } } + /// + /// + /// Legacy format: { "deviceKeys": ["deviceKey1", "deviceKey2"] } (equivalent to /fullStatus for each) + /// + /// Triggers the specified action paths for each device in parallel, /// then sends an /system/initialSyncComplete message after all have been processed. /// private void HandleBatchDeviceFullStatus(string clientId, JToken content) @@ -1513,11 +1519,37 @@ namespace PepperDash.Essentials return; } - var deviceKeys = content.SelectToken("deviceKeys")?.ToObject>(); + // Build a dictionary of deviceKey -> list of action paths + Dictionary> deviceActionPaths; - if (deviceKeys == null || deviceKeys.Count == 0) + var devicesToken = content.SelectToken("devices"); + if (devicesToken != null) { - this.LogWarning("BatchDeviceFullStatus: No device keys provided"); + // Granular format: { "devices": { "key": ["/path1", "/path2"], ... } } + deviceActionPaths = devicesToken.ToObject>>(); + } + else + { + // Legacy format: { "deviceKeys": ["key1", "key2"] } -> each gets /fullStatus + var deviceKeys = content.SelectToken("deviceKeys")?.ToObject>(); + + if (deviceKeys == null || deviceKeys.Count == 0) + { + this.LogWarning("BatchDeviceFullStatus: No device keys or devices provided"); + SendMessageObject(new MobileControlMessage + { + Type = "/system/initialSyncComplete", + ClientId = clientId + }); + return; + } + + deviceActionPaths = deviceKeys.ToDictionary(k => k, _ => new List { "/fullStatus" }); + } + + if (deviceActionPaths == null || deviceActionPaths.Count == 0) + { + this.LogWarning("BatchDeviceFullStatus: Empty devices dictionary"); SendMessageObject(new MobileControlMessage { Type = "/system/initialSyncComplete", @@ -1526,38 +1558,49 @@ namespace PepperDash.Essentials return; } - this.LogInformation("BatchDeviceFullStatus: Processing {count} device keys", deviceKeys.Count); + this.LogInformation("BatchDeviceFullStatus: Processing {count} devices", deviceActionPaths.Count); var tasks = new List(); - foreach (var deviceKey in deviceKeys) + foreach (var kvp in deviceActionPaths) { - var fullStatusPath = $"/device/{deviceKey}/fullStatus"; + var deviceKey = kvp.Key; + var actionPaths = kvp.Value; - var handlers = _actionDictionary - .Where(kv => fullStatusPath.StartsWith(kv.Key + "/")) - .SelectMany(kv => kv.Value) - .ToList(); - - if (handlers.Count == 0) + if (actionPaths == null || actionPaths.Count == 0) { - this.LogDebug("BatchDeviceFullStatus: No handlers for {deviceKey}", deviceKey); continue; } - foreach (var handler in handlers) + foreach (var actionPath in actionPaths) { - tasks.Add(Task.Run(() => + var fullPath = $"/device/{deviceKey}{actionPath}"; + + var handlers = _actionDictionary + .Where(kv => fullPath.StartsWith(kv.Key + "/")) + .SelectMany(kv => kv.Value) + .ToList(); + + if (handlers.Count == 0) { - try + this.LogDebug("BatchDeviceFullStatus: No handlers for {deviceKey} at path {actionPath}", deviceKey, actionPath); + continue; + } + + foreach (var handler in handlers) + { + tasks.Add(Task.Run(() => { - handler.Action(fullStatusPath, clientId, JToken.FromObject(new { deviceKey })); - } - catch (Exception ex) - { - this.LogError("BatchDeviceFullStatus: Exception in handler for {deviceKey}: {message}", deviceKey, ex.Message); - } - })); + try + { + handler.Action(fullPath, clientId, JToken.FromObject(new { deviceKey })); + } + catch (Exception ex) + { + this.LogError("BatchDeviceFullStatus: Exception in handler for {deviceKey} at {actionPath}: {message}", deviceKey, actionPath, ex.Message); + } + })); + } } } From b945ecb470c4973777cf39871ea919970878b3ad Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 25 Jun 2026 20:30:48 -0600 Subject: [PATCH 105/161] feat: add Dial method with password support and implement ICodecCallControlsMessenger for call control messaging --- .../Codec/Interfaces/ICodecCallControls.cs | 7 +++++++ .../VideoCodec/MockVC/MockVC.cs | 20 ++++++++++++++++++ .../VideoCodec/VideoCodecBase.cs | 2 ++ ...nger.cs => ICodecCallControlsMessenger.cs} | 21 ++++++++++++++++--- .../Messengers/IHasDirectoryMessenger.cs | 2 +- .../IHasScheduleAwarenessMessenger.cs | 6 +++--- .../MessengerFactoryRegistry.cs | 7 ++++++- 7 files changed, 57 insertions(+), 8 deletions(-) rename src/PepperDash.Essentials.MobileControl.Messengers/Messengers/{IHasCallControlsMessenger.cs => ICodecCallControlsMessenger.cs} (82%) diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/Interfaces/ICodecCallControls.cs b/src/PepperDash.Essentials.Devices.Common/Codec/Interfaces/ICodecCallControls.cs index 6110bb1a..b0d9bad1 100644 --- a/src/PepperDash.Essentials.Devices.Common/Codec/Interfaces/ICodecCallControls.cs +++ b/src/PepperDash.Essentials.Devices.Common/Codec/Interfaces/ICodecCallControls.cs @@ -18,4 +18,11 @@ public interface ICodecCallControls : IHasDialer /// /// The meeting to dial void Dial(Meeting meeting); + + /// + /// Dials the specified number with an optional password + /// + /// The number to dial + /// The optional password for the call + void Dial(string number, string password); } diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs index 56a4106e..dfa7a9cc 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs @@ -205,6 +205,26 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec } + public override void Dial(string number, string password) + { + Debug.LogMessage(LogEventLevel.Debug, this, "Dial: {0} with password: {1}", number, password); + var call = new CodecActiveCallItem() { Name = number, Number = number, Id = number, Status = eCodecCallStatus.Dialing, Direction = eCodecCallDirection.Outgoing, Type = eCodecCallType.Video }; + ActiveCalls.Add(call); + OnCallStatusChange(call); + //ActiveCallCountFeedback.FireUpdate(); + // Simulate 2-second ring, then connecting, then connected + var dialTimer = new Timer(2000) { AutoReset = false }; + dialTimer.Elapsed += (s, e) => + { + call.Type = eCodecCallType.Video; + SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call); + var connectTimer = new Timer(1000) { AutoReset = false }; + connectTimer.Elapsed += (ss, ee) => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call); + connectTimer.Start(); + }; + dialTimer.Start(); + } + /// public override void EndCall(CodecActiveCallItem call) { diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index 98a44b40..fceee089 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -272,6 +272,8 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingSource, /// public abstract void Dial(string number); + public abstract void Dial(string number, string password); + /// /// Ends the specified call /// diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICodecCallControlsMessenger.cs similarity index 82% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallControlsMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICodecCallControlsMessenger.cs index c28cbede..8b70d9ee 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICodecCallControlsMessenger.cs @@ -14,12 +14,12 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Provides a messaging bridge for devices implementing /// - public class ICallControlsMessenger : MessengerBase + public class ICodecCallControlsMessenger : MessengerBase { private readonly ICodecCallControls _callControls; - /// Initializes a new instance of the class. - public ICallControlsMessenger(string key, string messagePath, EssentialsDevice device) + /// Initializes a new instance of the class. + public ICodecCallControlsMessenger(string key, string messagePath, EssentialsDevice device) : base(key, messagePath, device) { _callControls = device as ICodecCallControls ?? throw new ArgumentNullException(nameof(device)); @@ -38,6 +38,12 @@ namespace PepperDash.Essentials.AppServer.Messengers AddAction("/dialMeeting", (id, content) => _callControls.Dial(content.ToObject())); + AddAction("/dialNumber", (id, content) => + { + var message = content.ToObject(); + _callControls.Dial(message.Number, message.Password); + }); + AddAction("/endCallById", (id, content) => { var s = content.ToObject>(); @@ -113,4 +119,13 @@ namespace PepperDash.Essentials.AppServer.Messengers public List Calls { get; set; } } + + public class ICallControlsDialMeetingMessage + { + [JsonProperty("number", Required = Required.Always)] + public string Number { get; set; } + + [JsonProperty("password", NullValueHandling = NullValueHandling.Ignore)] + public string Password { get; set; } + } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs index 31d61da6..a19526fc 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs @@ -143,7 +143,7 @@ namespace PepperDash.Essentials.AppServer.Messengers HasDirectory = true, HasDirectorySearch = true, DirectorySelectedFolderName = _directory.CurrentDirectoryResult?.CurrentDirectoryResults?.Count > 0 ? _directory.CurrentDirectoryResult.CurrentDirectoryResults[0].Name : null, - DirectorySelectedFolderIsNotRoot = _directory.CurrentDirectorResultIsNotDirectoryRoot?.BoolValue + DirectorySelectedFolderIsNotRoot = _directory.CurrentDirectoryResultIsNotDirectoryRoot?.BoolValue }); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs index affd2d95..563fa3bd 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs @@ -21,7 +21,7 @@ public class IHasScheduleAwarenessMessenger : MessengerBase protected override void RegisterActions() { - AddAction("/schedule/fullStatus", (id, content) => SendFullScheduleObject()); + AddAction("/schedule/fullStatus", (id, content) => SendFullScheduleObject(id)); } private void CodecSchedule_MeetingEventChange(object sender, MeetingEventArgs e) @@ -45,13 +45,13 @@ public class IHasScheduleAwarenessMessenger : MessengerBase /// /// Helper method to send the full schedule data /// - private void SendFullScheduleObject() + private void SendFullScheduleObject(string id = null) { PostStatusMessage(new FullScheduleMessage { Meetings = ScheduleSource.CodecSchedule.Meetings, MeetingWarningMinutes = ScheduleSource.CodecSchedule.MeetingWarningMinutes - }); + }, id); } } diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index e5c7e19a..d4fd816c 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -172,9 +172,14 @@ namespace PepperDash.Essentials ), new MessengerFactoryEntry( typeof(ICodecCallControls), - (d, mp, ck) => new ICallControlsMessenger( + (d, mp, ck) => new ICodecCallControlsMessenger( $"{d.Key}-callControls-{ck}", mp, d) ), + new MessengerFactoryEntry( + typeof(IHasScheduleAwareness), + (d, mp, ck) => new IHasScheduleAwarenessMessenger( + $"{d.Key}-schedule-{ck}", (IHasScheduleAwareness)d, mp) + ), new MessengerFactoryEntry( typeof(IHasContentSharing), (d, mp, ck) => new IHasContentSharingMessenger( From aae41b5947c2dfe2528d6cdea055794b7477cc07 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 26 Jun 2026 15:53:51 -0600 Subject: [PATCH 106/161] feat: refactor messaging interfaces and remove subscription support from IMobileControlMessengerWithSubscriptions --- .../IMobileControlMessenger.cs | 6 ++ ...MobileControlMessengerWithSubscriptions.cs | 17 +--- .../Messengers/IHasCallHistoryMessenger.cs | 10 +-- .../Messengers/IHasCodecCamerasMessenger.cs | 8 +- .../Messengers/IHasCodecLayoutsMessenger.cs | 8 +- .../Messengers/IHasCodecSelfViewMessenger.cs | 12 +-- .../Messengers/IHasDirectoryMessenger.cs | 10 +-- .../Messengers/MessengerBase.cs | 86 +++++-------------- .../MobileControlConfig.cs | 7 -- .../MobileControlSystemController.cs | 17 +--- 10 files changed, 60 insertions(+), 121 deletions(-) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessenger.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessenger.cs index 178289e4..bf34dcfb 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessenger.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessenger.cs @@ -27,5 +27,11 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces /// /// void RegisterWithAppServer(IMobileControl appServerController); + + /// + /// Unsubscribe a client from this messenger + /// + /// Client ID to remove from subscription list + void UnsubscribeClient(string clientId); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs index 887f1789..aeb95bc2 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs @@ -1,23 +1,14 @@ +using System; using PepperDash.Core; namespace PepperDash.Essentials.Core.DeviceTypeInterfaces { /// - /// Defines the contract for IMobileControlMessenger + /// Obsolete. Use directly. + /// Subscriptions are now always enabled in MessengerBase. /// + [Obsolete("Use IMobileControlMessenger directly. Subscriptions are always enabled.")] public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger { - /// - /// Unsubscribe a client from this messenger - /// - /// - void UnsubscribeClient(string clientId); - - /// - /// Register this messenger with the AppServerController - /// - /// parent for this messenger - /// Enable messenger subscriptions - void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs index 52bf9cce..fa746da9 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCallHistoryMessenger.cs @@ -30,10 +30,10 @@ namespace PepperDash.Essentials.AppServer.Messengers { base.RegisterActions(); - AddAction("/fullStatus", (id, content) => PostCallHistory()); - AddAction("/callHistoryStatus", (id, content) => PostCallHistory()); + AddAction("/fullStatus", (id, content) => PostCallHistory(id)); + AddAction("/callHistoryStatus", (id, content) => PostCallHistory(id)); - AddAction("/getCallHistory", (id, content) => PostCallHistory()); + AddAction("/getCallHistory", (id, content) => PostCallHistory(id)); } private void CallHistory_RecentCallsListHasChanged(object sender, EventArgs e) @@ -57,7 +57,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } } - private void PostCallHistory() + private void PostCallHistory(string id = null) { try { @@ -67,7 +67,7 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(new IHasCallHistoryStateMessage { RecentCalls = recents, - }); + }, id); } } catch (Exception ex) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs index 75e0c2e0..b0f06e0c 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecCamerasMessenger.cs @@ -40,8 +40,8 @@ namespace PepperDash.Essentials.AppServer.Messengers _cameraCodec.CameraSelected += CameraCodec_CameraSelected; - AddAction("/fullStatus", (id, content) => SendFullStatus()); - AddAction("/codecCamerasStatus", (id, content) => SendFullStatus()); + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + AddAction("/codecCamerasStatus", (id, content) => SendFullStatus(id)); AddAction("/cameraSelect", (id, content) => { @@ -327,7 +327,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } } - private void SendFullStatus() + private void SendFullStatus(string id = null) { try { @@ -344,7 +344,7 @@ namespace PepperDash.Essentials.AppServer.Messengers SelectedCamera = GetSelectedCamera(_cameraCodec) }, Presets = GetCurrentPresets() - }); + }, id); } catch (Exception ex) { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecLayoutsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecLayoutsMessenger.cs index db9cea4d..9a71c02a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecLayoutsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecLayoutsMessenger.cs @@ -28,12 +28,12 @@ namespace PepperDash.Essentials.AppServer.Messengers SendFullStatus(); } - private void SendFullStatus() + private void SendFullStatus(string id = null) { PostStatusMessage(new IHasCodecLayoutsStateMessage { CurrentLayout = _layouts.LocalLayoutFeedback.StringValue - }); + }, id); } /// @@ -41,9 +41,9 @@ namespace PepperDash.Essentials.AppServer.Messengers { base.RegisterActions(); - AddAction("/fullStatus", (id, content) => SendFullStatus()); + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); - AddAction("/layoutStatus", (id, content) => SendFullStatus()); + AddAction("/layoutStatus", (id, content) => SendFullStatus(id)); AddAction("/cameraRemoteView", (id, content) => _layouts.LocalLayoutToggle()); AddAction("/cameraLayout", (id, content) => _layouts.LocalLayoutToggle()); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs index 85e103f7..0b534dc5 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCodecSelfViewMessenger.cs @@ -28,9 +28,9 @@ namespace PepperDash.Essentials.AppServer.Messengers { base.RegisterActions(); - AddAction("/fullStatus", (id, content) => SendFullStatus()); + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); - AddAction("/cameraSelfViewStatus", (id, content) => SendFullStatus()); + AddAction("/cameraSelfViewStatus", (id, content) => SendFullStatus(id)); AddAction("/cameraSelfView", (id, content) => _selfView.SelfViewModeToggle()); @@ -42,12 +42,12 @@ namespace PepperDash.Essentials.AppServer.Messengers PostCameraSelfView(); } - private void SendFullStatus() + private void SendFullStatus(string id = null) { - PostCameraSelfView(); + PostCameraSelfView(id); } - private void PostCameraSelfView() + private void PostCameraSelfView(string id = null) { try { @@ -55,7 +55,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { CameraSelfViewIsOn = _selfView.SelfviewIsOnFeedback.BoolValue, ShowSelfViewByDefault = _selfView.ShowSelfViewByDefault - }); + }, id); } catch (Exception ex) { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs index a19526fc..fd4db6b4 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs @@ -30,9 +30,9 @@ namespace PepperDash.Essentials.AppServer.Messengers { base.RegisterActions(); - AddAction("/fullStatus", (id, content) => SendFullStatus()); + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); - AddAction("/directoryStatus", (id, content) => SendFullStatus()); + AddAction("/directoryStatus", (id, content) => SendFullStatus(id)); AddAction("/getDirectory", (id, content) => GetDirectoryRoot()); @@ -49,7 +49,7 @@ namespace PepperDash.Essentials.AppServer.Messengers AddAction("/directorySearch", (id, content) => { var msg = content.ToObject>(); - GetDirectory(msg.Value); + _directory.SearchDirectory(msg.Value); }); AddAction("/directoryBack", (id, content) => GetPreviousDirectory()); @@ -133,7 +133,7 @@ namespace PepperDash.Essentials.AppServer.Messengers _directory.GetDirectoryParentFolderContents(); } - private void SendFullStatus() + private void SendFullStatus(string id = null) { PostStatusMessage(new IHasDirectoryStateMessage { @@ -144,7 +144,7 @@ namespace PepperDash.Essentials.AppServer.Messengers HasDirectorySearch = true, DirectorySelectedFolderName = _directory.CurrentDirectoryResult?.CurrentDirectoryResults?.Count > 0 ? _directory.CurrentDirectoryResult.CurrentDirectoryResults[0].Name : null, DirectorySelectedFolderIsNotRoot = _directory.CurrentDirectoryResultIsNotDirectoryRoot?.BoolValue - }); + }, id); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index 88c61331..d61dc6a2 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Crestron.SimplSharp.Net; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Core.Logging; @@ -13,18 +12,13 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Provides a messaging bridge /// - public abstract class MessengerBase : EssentialsDevice, IMobileControlMessengerWithSubscriptions + public abstract class MessengerBase : EssentialsDevice, IMobileControlMessenger { /// /// The device this messenger is associated with /// protected IKeyName _device; - /// - /// Enable subscriptions - /// - protected bool enableMessengerSubscriptions; - /// /// List of clients subscribed to this messenger /// @@ -100,21 +94,6 @@ namespace PepperDash.Essentials.AppServer.Messengers RegisterActions(); } - /// - /// Register this messenger with appserver controller - /// - /// Parent controller for this messenger - /// Enable subscriptions - public void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions) - { - this.enableMessengerSubscriptions = enableMessengerSubscriptions; - AppServerController = appServerController ?? throw new ArgumentNullException("appServerController"); - - AppServerController.AddAction(this, HandleMessage); - - RegisterActions(); - } - private void HandleMessage(string path, string id, JToken content) { // replace base path with empty string. Should leave something like /fullStatus @@ -125,6 +104,12 @@ namespace PepperDash.Essentials.AppServer.Messengers return; } + // Auto-subscribe the client on any incoming action + if (!string.IsNullOrEmpty(id)) + { + SubscribeClient(id); + } + this.LogDebug("Executing action for path {path}", path); action(id, content); @@ -176,21 +161,15 @@ namespace PepperDash.Essentials.AppServer.Messengers } /// - /// Add client to the susbscription list for unsolicited feedback + /// Add client to the subscription list for unsolicited feedback /// /// Client ID to add - protected void SubscribeClient(string clientId) + private void SubscribeClient(string clientId) { - if (!enableMessengerSubscriptions) - { - return; - } - lock (_subscriberLock) { if (!subscriberIds.Add(clientId)) { - this.LogVerbose("Client {clientId} already subscribed", clientId); return; } } @@ -204,24 +183,14 @@ namespace PepperDash.Essentials.AppServer.Messengers /// Client ID to remove public void UnsubscribeClient(string clientId) { - if (!enableMessengerSubscriptions) - { - return; - } - bool wasSubscribed; lock (_subscriberLock) { - wasSubscribed = subscriberIds.Contains(clientId); - if (wasSubscribed) - { - subscriberIds.Remove(clientId); - } + wasSubscribed = subscriberIds.Remove(clientId); } if (!wasSubscribed) { - this.LogVerbose("Client with ID {clientId} is not subscribed", clientId); return; } @@ -301,35 +270,26 @@ namespace PepperDash.Essentials.AppServer.Messengers { try { - // Allow for legacy method to continue without subscriptions - if (!enableMessengerSubscriptions) + var messageType = !string.IsNullOrEmpty(type) ? type : MessagePath; + + // If clientId is provided, send directly to that client + if (!string.IsNullOrEmpty(clientId)) { - AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content }); + AppServerController?.SendMessageObject(new MobileControlMessage { Type = messageType, ClientId = clientId, Content = content }); return; } - // handle subscription feedback - // If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties - if (string.IsNullOrEmpty(clientId)) + // Unsolicited feedback: send to all subscribers + List subscriberSnapshot; + lock (_subscriberLock) { - // 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 }); - } - - return; + subscriberSnapshot = new List(subscriberIds); } - SubscribeClient(clientId); - - AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content }); + foreach (var client in subscriberSnapshot) + { + AppServerController?.SendMessageObject(new MobileControlMessage { Type = messageType, ClientId = client, Content = content }); + } } catch (Exception ex) { diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs index b74e30cf..05355e25 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs @@ -38,13 +38,6 @@ namespace PepperDash.Essentials ///
[JsonProperty("enableApiServer")] public bool EnableApiServer { get; set; } = true; - - /// - /// Enable subscriptions for Messengers. - /// Defaults to true for v3.x+ - /// - [JsonProperty("enableMessengerSubscriptions")] - public bool EnableMessengerSubscriptions { get; set; } = true; } /// diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 129af42f..f5c5f096 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -59,14 +59,14 @@ namespace PepperDash.Essentials new Dictionary(); /// - /// Get the custom messengers with subscriptions + /// Get the custom messengers /// - public ReadOnlyDictionary Messengers => new ReadOnlyDictionary(_messengers.Values.OfType().ToDictionary(k => k.Key, v => v)); + public ReadOnlyDictionary Messengers => new ReadOnlyDictionary(_messengers); /// /// Get the default messengers /// - public ReadOnlyDictionary DefaultMessengers => new ReadOnlyDictionary(_defaultMessengers.Values.OfType().ToDictionary(k => k.Key, v => v)); + public ReadOnlyDictionary DefaultMessengers => new ReadOnlyDictionary(_defaultMessengers); private readonly GenericQueue _transmitToServerQueue; @@ -534,23 +534,12 @@ namespace PepperDash.Essentials messenger.MessagePath ); - if (messenger is IMobileControlMessengerWithSubscriptions subMessenger) - { - subMessenger.RegisterWithAppServer(this, Config.EnableMessengerSubscriptions); - return; - } - messenger.RegisterWithAppServer(this); } /// protected 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 4bb2917b69aef62f8d051fe5469fe2503ffa103f Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 30 Jun 2026 17:00:26 -0600 Subject: [PATCH 107/161] feat: add RouteDescriptorCollectionChanged event to notify changes in route descriptors --- .../Routing/RouteDescriptorCollection.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs index 9cf61f77..f75196a2 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs @@ -1,4 +1,5 @@ -using PepperDash.Core; +using System; +using PepperDash.Core; using Serilog.Events; using System.Collections.Generic; using System.Linq; @@ -27,6 +28,8 @@ public class RouteDescriptorCollection private readonly List RouteDescriptors = new List(); + public event EventHandler RouteDescriptorCollectionChanged; + /// /// Gets an enumerable collection of all RouteDescriptors in this collection. /// @@ -70,6 +73,8 @@ public class RouteDescriptorCollection descriptor?.InputPort?.Key ?? "auto", descriptor?.SignalType); RouteDescriptors.Add(descriptor); + + RouteDescriptorCollectionChanged?.Invoke(this, EventArgs.Empty); } /// @@ -110,7 +115,10 @@ public class RouteDescriptorCollection ? GetRouteDescriptorForDestination(destination) : GetRouteDescriptorForDestinationAndInputPort(destination, inputPortKey); if (descr != null) + { RouteDescriptors.Remove(descr); + RouteDescriptorCollectionChanged?.Invoke(this, EventArgs.Empty); + } Debug.LogMessage(LogEventLevel.Information, "Found route descriptor {routeDescriptor}", destination, descr); From fb4ba8ccfad247ac344d0faab7c1eacc39b040cc Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 1 Jul 2026 17:14:32 -0600 Subject: [PATCH 108/161] feat: enhance routing functionality with additional parameters in RouteDescriptor and update RouteDescriptorCollection for change notifications --- .../Routing/Extensions.cs | 15 +- .../Routing/RouteDescriptor.cs | 247 ++++++++++-------- .../Routing/RouteDescriptorCollection.cs | 3 + .../Routing/RouteSwitchDescriptor.cs | 1 + .../Routing/RoutingFeedbackManager.cs | 105 ++------ 5 files changed, 171 insertions(+), 200 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 1e31127d..f12f7f53 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -222,7 +222,7 @@ public static class Extensions // if it's a single signal type, find the route if (!signalType.HasFlag(eRoutingSignalType.AudioVideo)) { - var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType); + var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, signalType); Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key, signalType); if (!destination.GetRouteToSource(source, null, null, signalType, 0, singleTypeRouteDescriptor, destinationPort, sourcePort)) @@ -240,14 +240,14 @@ public static class Extensions Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key); - var audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio); + var audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Audio); var audioSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort); if (!audioSuccess) Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key); - var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Video); + var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Video); var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, videoRouteDescriptor, destinationPort, sourcePort); @@ -366,7 +366,8 @@ public static class Extensions audioOrSingleRoute = audioCollection.Descriptors.FirstOrDefault(d => d.Source.Key == request.Source.Key && d.Destination.Key == request.Destination.Key && - (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key) && + (request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key)); } if (RouteDescriptors.TryGetValue(eRoutingSignalType.Video, out RouteDescriptorCollection videoCollection)) @@ -374,7 +375,8 @@ public static class Extensions videoRoute = videoCollection.Descriptors.FirstOrDefault(d => d.Source.Key == request.Source.Key && d.Destination.Key == request.Destination.Key && - (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key) && + (request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key)); } } else @@ -385,7 +387,8 @@ public static class Extensions audioOrSingleRoute = collection.Descriptors.FirstOrDefault(d => d.Source.Key == request.Source.Key && d.Destination.Key == request.Destination.Key && - (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key) && + (request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key)); } } diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs index 631aff62..32822d0d 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs @@ -7,137 +7,160 @@ using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core; - -/// -/// Represents a collection of individual route steps between a Source and a Destination device for a specific signal type. -/// -public class RouteDescriptor +namespace PepperDash.Essentials.Core { /// - /// The destination device (sink or midpoint) for the route. + /// Represents a collection of individual route steps between a Source and a Destination device for a specific signal type. /// - public IRoutingInputs Destination { get; private set; } - - /// - /// The specific input port on the destination device used for this route. Can be null if not specified or applicable. - /// - public RoutingInputPort InputPort { get; private set; } - - /// - /// The source device for the route. - /// - public IRoutingOutputs Source { get; private set; } - - /// - /// The type of signal being routed (e.g., Audio, Video). This descriptor represents a single signal type. - /// - public eRoutingSignalType SignalType { get; private set; } - - /// - /// A list of individual switching steps required to establish the route. - /// - public List Routes { get; private set; } - - /// - /// Initializes a new instance of the class for a route without a specific destination input port. - /// - /// The source device. - /// The destination device. - /// The type of signal being routed. - public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) : this(source, destination, null, signalType) + public class RouteDescriptor { - } + /// + /// The destination device (sink or midpoint) for the route. + /// + public IRoutingInputs Destination { get; private set; } - /// - /// Initializes a new instance of the class for a route with a specific destination input port. - /// - /// The source device. - /// The destination device. - /// The destination input port (optional). - /// The signal type for this route. - public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType) - { - Destination = destination; - InputPort = inputPort; - Source = source; - SignalType = signalType; - Routes = new List(); - } + /// + /// The InputPort on the destination device for this route, if applicable. May be null if the route is not for a specific input port. + /// + public RoutingInputPort InputPort { get; private set; } - /// - /// Executes all the switching steps defined in the list. - /// - public void ExecuteRoutes() - { - foreach (var route in Routes) + /// + /// Gets the source device (sink or midpoint) for the route. + /// + public IRoutingOutputs Source { get; private set; } + + /// + /// Gets the OutputPort on the source device for this route, if applicable. May be null if the route is not for a specific output port. + /// + public RoutingOutputPort OutputPort { get; private set; } + + /// + /// Gets the signal type for this route. + /// + public eRoutingSignalType SignalType { get; private set; } + + /// + /// Gets the collection of route switch descriptors for this route. + /// + public List Routes { get; private set; } + + /// + /// Initializes a new instance of the class for a route without a specific destination input port. + /// + /// The source device. + /// The destination device. + /// The type of signal being routed. + public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) : this(source, destination, null, signalType) { - Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString()); - - if (route.SwitchingDevice is IRoutingSinkWithFeedback sink) - { - sink.ExecuteSwitch(route.InputPort.Selector); - continue; - } - - if (route.SwitchingDevice is IRoutingMidpointWithFeedback switchingDevice) - { - switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); - - route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); - - Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } } - } - /// - /// Releases the usage tracking for the route and optionally clears the route on the switching devices. - /// - /// If true, attempts to clear the route on the switching devices (e.g., set input to null/0). - public void ReleaseRoutes(bool clearRoute = false) - { - foreach (var route in Routes.Where(r => r.SwitchingDevice is IRoutingMidpointWithFeedback)) + /// + /// Initializes a new instance of the class for a route with a specific destination input port. + /// + /// The source device. + /// The destination device. + /// The destination input port (optional). + /// The signal type for this route. + public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType) : this(source, destination, inputPort, null, signalType) { - if (route.SwitchingDevice is IRoutingMidpointWithFeedback switchingDevice) - { - if (clearRoute) - { - try - { - switchingDevice.ExecuteSwitch(null, route.OutputPort.Selector, SignalType); - } - catch (Exception e) - { - Debug.LogError("Error executing switch: {exception}", e.Message); - } - } + } - if (route.OutputPort == null) + /// + /// Initializes a new instance of the class for a route with specific destination input and source output ports. + /// + /// + /// + /// + /// + /// + public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, RoutingOutputPort outputPort, eRoutingSignalType signalType) + { + Destination = destination; + InputPort = inputPort; + Source = source; + OutputPort = outputPort; + SignalType = signalType; + Routes = new List(); + } + + /// + /// ExecuteRoutes method + /// + public void ExecuteRoutes() + { + foreach (var route in Routes) + { + Debug.LogVerbose("ExecuteRoutes: {0}", route.ToString()); + + if (route.SwitchingDevice is IRoutingSinkWithFeedback sink) { + sink.ExecuteSwitch(route.InputPort.Selector); continue; } - if (route.OutputPort.InUseTracker != null) + if (route.SwitchingDevice is IRoutingMidpointWithFeedback switchingDevice) { - route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); - Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } - else - { - Debug.LogMessage(LogEventLevel.Error, "InUseTracker is null for OutputPort {0}", null, route.OutputPort.Key); + switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); + + route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); + + Debug.LogVerbose("Output port {0} routing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); } } } + + /// + /// Releases the usage tracking for the route and optionally clears the route on the switching devices. + /// + /// If true, attempts to clear the route on the switching devices (e.g., set input to null/0). + + + public void ReleaseRoutes(bool clearRoute = false) + { + foreach (var route in Routes.Where(r => r.SwitchingDevice is IRoutingMidpointWithFeedback)) + { + if (route.SwitchingDevice is IRoutingMidpointWithFeedback switchingDevice) + { + if (clearRoute) + { + try + { + switchingDevice.ExecuteSwitch(null, route.OutputPort.Selector, SignalType); + } + catch (Exception e) + { + Debug.LogError("Error executing switch: {exception}", e.Message); + Debug.LogDebug(e, "Stack Trace: "); + } + } + + if (route.OutputPort == null) + { + continue; + } + + if (route.OutputPort.InUseTracker != null) + { + route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); + Debug.LogVerbose("Port {0} releasing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); + } + else + { + Debug.LogVerbose("InUseTracker is null for OutputPort {0}", route.OutputPort.Key); + } + } + } + } + + /// + /// Returns a string representation of the route descriptor, including source, destination, and individual route steps. + /// + /// A string describing the route. + public override string ToString() + { + var routesText = Routes.Select(r => r.ToString()).ToArray(); + return $"Route table from {Source.Key} to {Destination.Key} for {SignalType}:\r\n {string.Join("\r\n ", routesText)}"; + } } - /// - /// Returns a string representation of the route descriptor, including source, destination, and individual route steps. - /// - /// A string describing the route. - public override string ToString() - { - var routesText = Routes.Select(r => r.ToString()).ToArray(); - return $"Route table from {Source.Key} to {Destination.Key} for {SignalType}:\r\n {string.Join("\r\n ", routesText)}"; - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs index f75196a2..d17ca8fe 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs @@ -28,6 +28,9 @@ public class RouteDescriptorCollection private readonly List RouteDescriptors = new List(); + /// + /// Event raised when the collection of RouteDescriptors changes (add/remove). This is useful for updating routing status in the UI, for example. + /// public event EventHandler RouteDescriptorCollectionChanged; /// diff --git a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs index 38d56b93..2e526589 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs @@ -2,6 +2,7 @@ { /// /// Represents a RouteSwitchDescriptor + /// This represents a switch to be made on an IRoutingInputs device, which could be a matrix switcher or a sink device. /// public class RouteSwitchDescriptor { diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 15855cf4..0eb741bb 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -225,6 +225,7 @@ namespace PepperDash.Essentials.Core.Routing try { UpdateDestination(sender, currentInputPort); + } catch (Exception ex) { @@ -340,11 +341,6 @@ namespace PepperDash.Essentials.Core.Routing inputPort ); - var tempSourceListItem = new SourceListItem - { - SourceKey = "$transient", - Name = inputPort.Key, - }; return; } @@ -371,12 +367,16 @@ namespace PepperDash.Essentials.Core.Routing inputPort ); - var tempSourceListItem = new SourceListItem - { - SourceKey = "$transient", - Name = "None", - }; + // determine all the tie lines between the source and destination to determine the signal type + // the type is the union of all the tie lines between the source and destination + + // For now we assume the type matches the tie line connected to the destination + destination.SetCurrentSource(firstTieLine.Type, null); + + // remove existing descriptor if any + RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPort.Key); + return; } } @@ -386,83 +386,24 @@ namespace PepperDash.Essentials.Core.Routing return; } - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine); - // Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet. - var room = DeviceManager - .AllDevices.OfType() - .FirstOrDefault( - (r) => - { + // Get the routes from the destination to the source using the existing GetRouteToSource method + var routes = destination.GetRouteToSource( + sourceTieLine.SourcePort.ParentDevice as IRoutingOutputs, + sourceTieLine.Type, + inputPort, + sourceTieLine.SourcePort + ); - if (r is IHasDefaultDisplay roomDefaultDisplay) - { - return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; - } + // remove existing descriptor if any + RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPort.Key); - return false; - } - ); + // Add the new route descriptors to the collection + RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(routes.Item1); - if (room == null) + if(routes.Item2 != null) { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No room found for display {destination}", - this, - destination.Key - ); - return; - } - - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found room {room} for destination {destination}", this, room.Key, destination.Key); - - var sourceList = ConfigReader.ConfigObject.GetSourceListForKey(room.SourceListKey); - - if (sourceList == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", - this, - room.SourceListKey, - sourceTieLine - ); - return; - } - - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key); - - var sourceListItem = sourceList.FirstOrDefault(sli => - { - //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, - // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", - // this, - // sli.Key, - // sli.Value.SourceKey, - // sourceTieLine.SourcePort.ParentDevice.Key); - - return sli.Value.SourceKey.Equals( - sourceTieLine.SourcePort.ParentDevice.Key, - StringComparison.InvariantCultureIgnoreCase - ); - }); - - var source = sourceListItem.Value; - var sourceKey = sourceListItem.Key; - - if (source == null) - { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Debug, - "No source found for device {key}. Creating transient source for {destination}", - this, - sourceTieLine.SourcePort.ParentDevice.Key, - destination - ); - - - return; + RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(routes.Item2); } } From 9f18275f8179a4153eaed34b96acb4f28a61301d Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 2 Jul 2026 10:53:51 -0600 Subject: [PATCH 109/161] feat: update RoutingFeedbackManager to map downstream sink input ports and optimize performance in route updates --- .../Routing/RoutingFeedbackManager.cs | 81 ++++++++++++------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 0eb741bb..eac9b83a 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -14,9 +14,12 @@ namespace PepperDash.Essentials.Core.Routing public class RoutingFeedbackManager : EssentialsDevice { /// - /// Maps midpoint device keys to the set of sink device keys that are downstream + /// Maps midpoint device keys to the set of downstream sink input ports, derived from the + /// static tie-line topology. Because it is built from topology rather than a sink's currently + /// reported input, sinks that never report an input (i.e. + /// stays null, such as codecs fed from an external matrix) are still mapped and updated. /// - private Dictionary> midpointToSinksMap; + private Dictionary> midpointToSinkInputsMap; /// /// Debounce timers for each sink device to prevent rapid successive updates @@ -42,30 +45,39 @@ namespace PepperDash.Essentials.Core.Routing } /// - /// Builds a map of which sink devices are downstream of each midpoint device - /// for performance optimization in HandleMidpointUpdate + /// Builds a map of which sink input ports are downstream of each midpoint device + /// for performance optimization in HandleMidpointUpdate. + /// The map is derived from the static tie-line topology (every sink input port is traced + /// upstream), so it does not depend on a sink having already reported its current input. /// private void BuildMidpointSinkMap() { - midpointToSinksMap = new Dictionary>(); + midpointToSinkInputsMap = new Dictionary>(); var sinks = DeviceManager.AllDevices.OfType(); - var midpoints = DeviceManager.AllDevices.OfType(); foreach (var sink in sinks) { - if (sink.CurrentInputPort == null) + // Trace from every input port on the sink (static topology) rather than only the + // currently-selected input. Sinks that never report an input still get mapped. + var inputPorts = (sink as IRoutingInputs)?.InputPorts; + if (inputPorts == null) continue; - // Find all upstream midpoints for this sink - var upstreamMidpoints = GetUpstreamMidpoints(sink); - - foreach (var midpointKey in upstreamMidpoints) + foreach (var inputPort in inputPorts) { - if (!midpointToSinksMap.ContainsKey(midpointKey)) - midpointToSinksMap[midpointKey] = new HashSet(); + var upstreamMidpoints = GetUpstreamMidpointsForInput(inputPort); - midpointToSinksMap[midpointKey].Add(sink.Key); + foreach (var midpointKey in upstreamMidpoints) + { + if (!midpointToSinkInputsMap.TryGetValue(midpointKey, out var inputs)) + { + inputs = new HashSet(); + midpointToSinkInputsMap[midpointKey] = inputs; + } + + inputs.Add(inputPort); + } } } @@ -73,24 +85,25 @@ namespace PepperDash.Essentials.Core.Routing Serilog.Events.LogEventLevel.Information, "Built midpoint-to-sink map with {count} midpoints", this, - midpointToSinksMap.Count + midpointToSinkInputsMap.Count ); } /// - /// Gets all upstream midpoint device keys for a given sink + /// Gets all upstream midpoint device keys reachable from a specific sink input port + /// by walking the static tie-line topology. /// - private HashSet GetUpstreamMidpoints(IRoutingSinkWithFeedback sink) + private HashSet GetUpstreamMidpointsForInput(RoutingInputPort inputPort) { var result = new HashSet(); var visited = new HashSet(); - if (sink.CurrentInputPort == null) + if (inputPort == null) return result; var tieLine = TieLineCollection.Default.FirstOrDefault(tl => - tl.DestinationPort.Key == sink.CurrentInputPort.Key && - tl.DestinationPort.ParentDevice.Key == sink.CurrentInputPort.ParentDevice.Key); + tl.DestinationPort.Key == inputPort.Key && + tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key); if (tieLine == null) return result; @@ -171,22 +184,34 @@ namespace PepperDash.Essentials.Core.Routing try { // Only update affected sinks (performance optimization) - if (midpointToSinksMap != null && midpointToSinksMap.TryGetValue(midpoint.Key, out var affectedSinkKeys)) + if (midpointToSinkInputsMap != null && midpointToSinkInputsMap.TryGetValue(midpoint.Key, out var affectedInputPorts)) { Debug.LogMessage( Serilog.Events.LogEventLevel.Debug, - "Midpoint {midpoint} changed, updating {count} downstream sinks", + "Midpoint {midpoint} changed, updating {count} downstream sink inputs", this, midpoint.Key, - affectedSinkKeys.Count + affectedInputPorts.Count ); - foreach (var sinkKey in affectedSinkKeys) + // Avoid redundant updates when a feedback-reporting sink has several mapped inputs. + var updatedSinkKeys = new HashSet(); + + foreach (var inputPort in affectedInputPorts) { - if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithFeedback sink) - { - UpdateDestination(sink, sink.CurrentInputPort); - } + if (!(inputPort.ParentDevice is IRoutingSinkWithFeedback sink)) + continue; + + // Sinks that report their input drive updates off the currently-selected input. + // Sinks that never report an input (CurrentInputPort == null, e.g. a codec fed + // from an external matrix) fall back to the static topology input port so that + // matrix route changes still propagate. + var portToUse = sink.CurrentInputPort ?? inputPort; + + if (sink.CurrentInputPort != null && !updatedSinkKeys.Add(sink.Key)) + continue; + + UpdateDestination(sink, portToUse); } } else From d771320fff013e3462b08c8e65776536ab4fa1f4 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 2 Jul 2026 15:43:07 -0600 Subject: [PATCH 110/161] feat: refactor DeviceJsonApi parameter conversion and add MockRoutingMidpoint with configuration properties --- .../Devices/DeviceJsonApi.cs | 56 +++++- .../Displays/MockDisplay.cs | 7 +- .../Routing/MockRoutingMidpoint.cs | 177 ++++++++++++++++++ .../MockRoutingMidpointPropertiesConfig.cs | 53 ++++++ 4 files changed, 286 insertions(+), 7 deletions(-) create mode 100644 src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs create mode 100644 src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs index b2565b3d..b3e6d48f 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceJsonApi.cs @@ -78,9 +78,7 @@ public class DeviceJsonApi } var mParams = method.GetParameters(); - var convertedParams = mParams - .Select((p, i) => ConvertType(action.Params[i], p.ParameterType)) - .ToArray(); + var convertedParams = BuildConvertedParams(mParams, action.Params); Task.Run(() => { @@ -142,9 +140,7 @@ public class DeviceJsonApi } var mParams = method.GetParameters(); - var convertedParams = mParams - .Select((p, i) => ConvertType(action.Params[i], p.ParameterType)) - .ToArray(); + var convertedParams = BuildConvertedParams(mParams, action.Params); try { @@ -173,8 +169,56 @@ public class DeviceJsonApi } } + /// + /// Builds the ordered, converted argument array to pass to . + /// + /// + /// Handles the common positional case (one supplied param per method parameter), as well as a method + /// with a single array-typed parameter (e.g. void Foo(string[] items)). For the latter, config + /// authors may either supply the params as a flat list matching the array's contents + /// ("params": ["a", "b", "c"]) or as a single nested array ("params": [["a", "b", "c"]]) - + /// both are converted into the single array argument correctly. + /// + /// The target method's parameters. + /// The raw parameter values supplied in the action. + /// The converted arguments, in order, ready to pass to Invoke. + private static object[] BuildConvertedParams(ParameterInfo[] mParams, object[] actionParams) + { + if (mParams.Length == 1 && mParams[0].ParameterType.IsArray && actionParams.Length != 1) + { + return new[] { ConvertType(actionParams, mParams[0].ParameterType) }; + } + + return mParams + .Select((p, i) => ConvertType(actionParams[i], p.ParameterType)) + .ToArray(); + } + private static object ConvertType(object value, Type conversionType) { + if (conversionType.IsArray) + { + var elementType = conversionType.GetElementType(); + + // The supplied value is a collection of raw items (a JArray, or the whole flat params list) - + // convert each item to the array's element type. + if (value is IEnumerable enumerable && !(value is string)) + { + var items = enumerable.Cast() + .Select(item => ConvertType(item, elementType)) + .ToArray(); + + var typedArray = Array.CreateInstance(elementType, items.Length); + Array.Copy(items, typedArray, items.Length); + return typedArray; + } + + // A single scalar value was supplied for an array parameter - wrap it in a single-element array. + var singleArray = Array.CreateInstance(elementType, 1); + singleArray.SetValue(ConvertType(value, elementType), 0); + return singleArray; + } + if (!conversionType.IsEnum) { return Convert.ChangeType(value, conversionType, System.Globalization.CultureInfo.InvariantCulture); diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs index f176c24c..a7e30660 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs @@ -65,7 +65,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA int VolumeHeldRepeatInterval = 200; ushort VolumeInterval = 655; - ushort _FakeVolumeLevel = 31768; + ushort _FakeVolumeLevel = 32768; bool _IsMuted; Timer _volumeUpTimer; Timer _volumeDownTimer; @@ -107,6 +107,11 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA VolumeLevelFeedback = new IntFeedback("volume", () => { return _FakeVolumeLevel; }); MuteFeedback = new BoolFeedback("muteOn", () => _IsMuted); + // Seed the feedback's cached value from _FakeVolumeLevel immediately. IntFeedback.IntValue only + // reflects the current backing value after FireUpdate has run once, so without this the UI would + // report 0 until the volume was first incremented/decremented. + VolumeLevelFeedback.FireUpdate(); + WarmupTime = 10000; CooldownTime = 10000; } diff --git a/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs new file mode 100644 index 00000000..74853223 --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using Serilog.Events; + +namespace PepperDash.Essentials.Devices.Common.Routing; + +/// +/// A mock midpoint routing device (e.g. a matrix switcher) that implements +/// without any real hardware communication. Its input and output ports are configured via +/// , each with a name, signal type, and physical port +/// (connection) type - so it can stand in for a real switching device (such as a StreamSync matrix) for +/// development and testing of routing logic. +/// +[Description("A mock routing midpoint (e.g. matrix switcher) device for testing routing logic without real hardware")] +public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedback +{ + /// + /// The configuration properties for this device. + /// + public MockRoutingMidpointPropertiesConfig PropertiesConfig { get; private set; } + + /// + public RoutingPortCollection InputPorts { get; private set; } + + /// + public RoutingPortCollection OutputPorts { get; private set; } + + /// + public List CurrentRoutes { get; } = new List(); + + /// + public event RouteChangedEventHandler RouteChanged; + + /// + /// Initializes a new instance of the class from a . + /// + /// The device configuration, whose Properties are deserialized as . + public MockRoutingMidpoint(DeviceConfig config) + : base(config.Key, config.Name) + { + PropertiesConfig = config.Properties != null + ? JsonConvert.DeserializeObject(config.Properties.ToString()) + : null; + PropertiesConfig ??= new MockRoutingMidpointPropertiesConfig(); + + InputPorts = new RoutingPortCollection(); + OutputPorts = new RoutingPortCollection(); + + BuildPorts(); + } + + /// + /// Builds the input and output ports from . Each port's Key and Selector + /// are both set to the configured port name, so selectors passed to and + /// can simply be looked up by matching against the port's Selector. + /// + private void BuildPorts() + { + try + { + foreach (var portConfig in PropertiesConfig.InputPorts) + { + if (string.IsNullOrEmpty(portConfig.Name)) + { + this.LogWarning("Skipping input port with no name configured for {key}", Key); + continue; + } + + var port = new RoutingInputPort(portConfig.Name, portConfig.SignalType, portConfig.PortType, portConfig.Name, this); + InputPorts.Add(port); + } + + foreach (var portConfig in PropertiesConfig.OutputPorts) + { + if (string.IsNullOrEmpty(portConfig.Name)) + { + this.LogWarning("Skipping output port with no name configured for {key}", Key); + continue; + } + + var port = new RoutingOutputPort(portConfig.Name, portConfig.SignalType, portConfig.PortType, portConfig.Name, this); + OutputPorts.Add(port); + } + + this.LogInformation("Built {inputCount} input port(s) and {outputCount} output port(s) for mock midpoint {key}", + InputPorts.Count, OutputPorts.Count, Key); + } + catch (Exception ex) + { + this.LogException(ex, "Error building ports for mock midpoint {0}", Key); + } + } + + /// + public void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType) + { + try + { + var outputPort = OutputPorts.FirstOrDefault(p => Equals(p.Selector, outputSelector)); + + if (outputPort == null) + { + this.LogWarning("Unable to find output port for selector {selector} on {key}", outputSelector, Key); + return; + } + + // Remove any existing route to this output before making or clearing the new one. + var existingRoute = CurrentRoutes.FirstOrDefault(r => r.OutputPort?.Key == outputPort.Key); + if (existingRoute != null) + { + CurrentRoutes.Remove(existingRoute); + } + + if (inputSelector == null) + { + this.LogInformation("Clearing route to output {output} on {key}", outputPort.Key, Key); + + var clearedDescriptor = new RouteSwitchDescriptor(outputPort, null); + RouteChanged?.Invoke(this, clearedDescriptor); + return; + } + + var inputPort = InputPorts.FirstOrDefault(p => Equals(p.Selector, inputSelector)); + + if (inputPort == null) + { + this.LogWarning("Unable to find input port for selector {selector} on {key}", inputSelector, Key); + return; + } + + var descriptor = new RouteSwitchDescriptor(outputPort, inputPort); + CurrentRoutes.Add(descriptor); + + this.LogInformation("Executed switch: {input} -> {output} ({signalType}) on {key}", + inputPort.Key, outputPort.Key, signalType, Key); + + RouteChanged?.Invoke(this, descriptor); + } + catch (Exception ex) + { + this.LogException(ex, "Error executing switch on mock midpoint {0}", Key); + } + } + + /// + public void ClearRoute(object outputSelector, eRoutingSignalType signalType) + { + ExecuteSwitch(null, outputSelector, signalType); + } +} + +/// +/// Factory for building devices. +/// +public class MockRoutingMidpointFactory : EssentialsDeviceFactory +{ + /// + /// Initializes a new instance of the class. + /// + public MockRoutingMidpointFactory() + { + TypeNames = new List { "mockroutingmidpoint", "mockmidpoint" }; + } + + /// + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MockRoutingMidpoint Device"); + return new MockRoutingMidpoint(dc); + } +} diff --git a/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs new file mode 100644 index 00000000..7c7d4bc3 --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.Routing; + +/// +/// Configuration properties for a device. +/// +public class MockRoutingMidpointPropertiesConfig +{ + /// + /// The input ports to build on the mock midpoint. + /// + [JsonProperty("inputPorts")] + public List InputPorts { get; set; } = new List(); + + /// + /// The output ports to build on the mock midpoint. + /// + [JsonProperty("outputPorts")] + public List OutputPorts { get; set; } = new List(); +} + +/// +/// Configuration for a single input or output port on a device. +/// +public class MockRoutingMidpointPortConfig +{ + /// + /// The name of the port. Used as both the port's key and its selector value, so it must be unique + /// within the collection (input ports or output ports) it is configured under. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The routing signal type supported by this port (e.g. "Audio", "Video", "AudioVideo", "Usb"). + /// Defaults to AudioVideo if not specified. + /// + [JsonProperty("signalType")] + [JsonConverter(typeof(StringEnumConverter))] + public eRoutingSignalType SignalType { get; set; } = eRoutingSignalType.AudioVideo; + + /// + /// The physical connection type of this port (e.g. "Hdmi", "Dm", "DisplayPort"). Defaults to Hdmi if + /// not specified. + /// + [JsonProperty("portType")] + [JsonConverter(typeof(StringEnumConverter))] + public eRoutingPortConnectionType PortType { get; set; } = eRoutingPortConnectionType.Hdmi; +} From f3f7755735610ffb44088238abe09eaf30490efa Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 2 Jul 2026 17:21:06 -0600 Subject: [PATCH 111/161] feat: refine messenger factory to prevent conflicting state shapes for IDialerCallStatus and ICodecCallControls --- .../Messengers/IDialerCallStatusMessenger.cs | 1 - .../MessengerFactoryRegistry.cs | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs index d9eacecd..a1c6e5d7 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs @@ -32,7 +32,6 @@ namespace PepperDash.Essentials.AppServer.Messengers /// protected override void RegisterActions() - { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index d4fd816c..d2b5c73e 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -168,7 +168,13 @@ namespace PepperDash.Essentials new MessengerFactoryEntry( typeof(IHasDialer), (d, mp, ck) => new IHasDialerMessenger( - $"{d.Key}-dialer-{ck}", mp, d) + $"{d.Key}-dialer-{ck}", mp, d), + // IDialerCallStatus and ICodecCallControls both extend IHasDialer and each already + // provide their own richer messenger (posting a "calls" array) subscribed to the same + // CallStatusChange event and the same "/device/{key}" message path. Without this + // exclusion, a device implementing either interface would get both messengers posting + // conflicting state shapes ("callItem" vs "calls") to the same clients. + predicate: d => d is IHasDialer && !(d is IDialerCallStatus) && !(d is ICodecCallControls) ), new MessengerFactoryEntry( typeof(ICodecCallControls), From 7de2aa7c2ecef4ef6e89510de236a4957a649a95 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 7 Jul 2026 14:57:23 -0600 Subject: [PATCH 112/161] feat: update directory contacts and enhance directory messaging to include current root directory --- .../VideoCodec/MockVC/MockCodecDirectory.cs | 24 +++++++++---------- .../Messengers/IHasDirectoryMessenger.cs | 6 +++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockCodecDirectory.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockCodecDirectory.cs index 185f089f..e5699752 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockCodecDirectory.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockCodecDirectory.cs @@ -217,14 +217,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec new DirectoryContact() { ContactId = "nyc_2", - Name = "Sumanth Rayancha", + Name = "Priya Chandran", Title = @"CTO", ContactMethods = new List() { new ContactMethod() { ContactMethodId = "cid_1", - Number = "srayancha.pepperdash.com", + Number = "pchandran.pepperdash.com", Device = eContactMethodDevice.Video, CallType = eContactMethodCallType.Video } @@ -233,14 +233,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec new DirectoryContact() { ContactId = "nyc_3", - Name = "Justin Gordon", + Name = "Miles Anderson", Title = @"Software Developer", ContactMethods = new List() { new ContactMethod() { ContactMethodId = "cid_1", - Number = "jgordon.pepperdash.com", + Number = "manderson.pepperdash.com", Device = eContactMethodDevice.Video, CallType = eContactMethodCallType.Video } @@ -309,14 +309,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec new DirectoryContact() { ContactId = "sfo_1", - Name = "David Huselid", + Name = "Owen Bradshaw", Title = @"Cive President, COO", ContactMethods = new List() { new ContactMethod() { ContactMethodId = "cid_1", - Number = "dhuselid.pepperdash.com", + Number = "obradshaw.pepperdash.com", Device = eContactMethodDevice.Video, CallType = eContactMethodCallType.Video } @@ -347,14 +347,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec new DirectoryContact() { ContactId = "den_1", - Name = "Heath Volmer", + Name = "Derek Simmons", Title = @"Software Developer", ContactMethods = new List() { new ContactMethod() { ContactMethodId = "cid_1", - Number = "hvolmer.pepperdash.com", + Number = "dsimmons.pepperdash.com", Device = eContactMethodDevice.Video, CallType = eContactMethodCallType.Video } @@ -385,14 +385,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec new DirectoryContact() { ContactId = "atx_1", - Name = "Vincent Longano", + Name = "Marcus Webb", Title = @"Product Development Manager", ContactMethods = new List() { new ContactMethod() { ContactMethodId = "cid_1", - Number = "vlongano.pepperdash.com", + Number = "mwebb.pepperdash.com", Device = eContactMethodDevice.Video, CallType = eContactMethodCallType.Video } @@ -452,14 +452,14 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec new DirectoryContact() { ContactId = "cdn_1", - Name = "Neil Dorin", + Name = "Alex Turner", Title = @"Software Developer /SC", ContactMethods = new List() { new ContactMethod() { ContactMethodId = "cid_1", - Number = "ndorin@pepperdash.com", + Number = "aturner@pepperdash.com", Device = eContactMethodDevice.Video, CallType = eContactMethodCallType.Video } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs index fd4db6b4..3c87a5bc 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasDirectoryMessenger.cs @@ -78,6 +78,11 @@ namespace PepperDash.Essentials.AppServer.Messengers this.LogVerbose("Sending Directory. Directory Item Count: {directoryItemCount}", directory.CurrentDirectoryResults.Count); Task.Run(() => PostStatusMessage(new IHasDirectoryStateMessage { + // DirectoryRoot is otherwise only sent once, in response to an explicit /fullStatus + // request. Since the root directory can keep growing after that (e.g. contacts + // trickling in from a paged/async phonebook sync), include it here too so it stays + // current as new directory results arrive, not just on first subscribe. + DirectoryRoot = _directory.DirectoryRoot, CurrentDirectory = directory })); } @@ -93,6 +98,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { PostStatusMessage(new IHasDirectoryStateMessage { + DirectoryRoot = _directory.DirectoryRoot, InitialPhonebookSyncComplete = true }); } From 34c82588f09f760b2d9683ac25422dd984d6b808 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 7 Jul 2026 15:10:08 -0600 Subject: [PATCH 113/161] feat: add additional HDMI output ports to MockVC for enhanced routing capabilities --- .../VideoCodec/MockVC/MockVC.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs index dfa7a9cc..4dcbee11 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs @@ -43,7 +43,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// /// Gets or sets the HdmiOut /// - public RoutingOutputPort HdmiOut { get; private set; } + public RoutingOutputPort HdmiOut1 { get; private set; } + + public RoutingOutputPort HdmiOut2 { get; private set; } /// /// Gets or sets the CallFavorites @@ -85,8 +87,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec InputPorts.Add(HdmiIn1); HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, 2, this); InputPorts.Add(HdmiIn2); - HdmiOut = new RoutingOutputPort(RoutingPortNames.HdmiOut, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, null, this); - OutputPorts.Add(HdmiOut); + HdmiOut1 = new RoutingOutputPort(RoutingPortNames.HdmiOut1, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, null, this); + OutputPorts.Add(HdmiOut1); + HdmiOut2 = new RoutingOutputPort(RoutingPortNames.HdmiOut2, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, null, this); + OutputPorts.Add(HdmiOut2); CallHistory = new CodecCallHistory(); for (int i = 0; i < 10; i++) From 122881100a8e5765c1dd3fa853ea0b7b719b14f0 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Jul 2026 14:39:50 -0600 Subject: [PATCH 114/161] feat: enhance MockVC to support participant management and add HDMI output port --- .../VideoCodec/MockVC/MockVC.cs | 110 +++++++++++++++++- .../MobileControlWebsocketServer.cs | 8 +- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs index 4dcbee11..d728eef4 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs @@ -14,6 +14,7 @@ using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Routing; using PepperDash.Essentials.Devices.Common.Cameras; using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; using Serilog.Events; namespace PepperDash.Essentials.Devices.Common.VideoCodec @@ -21,7 +22,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// /// Represents a MockVC /// - public class MockVC : VideoCodecBase, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingSinkWithFeedback + public class MockVC : VideoCodecBase, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, + IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingSinkWithFeedback, IHasParticipants { /// /// Gets or sets the PropertiesConfig @@ -45,6 +47,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// public RoutingOutputPort HdmiOut1 { get; private set; } + /// + /// Gets or sets the HdmiOut2 + /// public RoutingOutputPort HdmiOut2 { get; private set; } /// @@ -52,8 +57,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// public CodecCallFavorites CallFavorites { get; private set; } + private readonly CodecParticipants _participants; + /// - /// + /// Initializes a new instance of the class. /// public MockVC(DeviceConfig config) : base(config) @@ -71,6 +78,23 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec DirectoryBrowseHistory = new List(); + _participants = new CodecParticipants(); + + // Populate a fake participants list whenever a call connects, and clear it once all + // calls have ended, so IHasParticipants consumers (e.g. the roster UI) have something + // to display without needing a real far-end connection. + CallStatusChange += (o, a) => + { + if (a.CallItem.Status == eCodecCallStatus.Connected) + { + PopulateFakeParticipants(); + } + else if (a.CallItem.Status == eCodecCallStatus.Disconnected && ActiveCalls.Count == 0) + { + Participants.CurrentParticipants = new List(); + } + }; + // Debug helpers MuteFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Debug, this, "Mute={0}", _IsMuted); PrivacyModeIsOnFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Debug, this, "Privacy={0}", _PrivacyModeIsOn); @@ -209,6 +233,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec } + /// public override void Dial(string number, string password) { Debug.LogMessage(LogEventLevel.Debug, this, "Dial: {0} with password: {1}", number, password); @@ -652,6 +677,65 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec #endregion + #region IHasParticipants Members + + /// + /// Gets the collection of call participants + /// + public CodecParticipants Participants + { + get { return _participants; } + } + + /// + /// Builds a fake set of participants and assigns it to , + /// simulating a codec that has joined a call with other people already present. + /// + private void PopulateFakeParticipants() + { + Participants.CurrentParticipants = new List + { + new Participant { UserId = 1, Name = "Myself", IsHost = true, IsMyself = true, CanMuteAudio = true, CanUnmuteVideo = true }, + new Participant { UserId = 2, Name = "Priya Chandran", CanMuteAudio = true, CanUnmuteVideo = true }, + new Participant { UserId = 3, Name = "Miles Anderson", CanMuteAudio = true, CanUnmuteVideo = true, AudioMuteFb = true }, + new Participant { UserId = 4, Name = "Owen Bradshaw", CanMuteAudio = true, CanUnmuteVideo = true, HandIsRaisedFb = true }, + }; + } + + /// + public void RemoveParticipant(int userId) + { + Debug.LogMessage(LogEventLevel.Debug, this, "RemoveParticipant: {0}", userId); + + var remaining = Participants.CurrentParticipants.Where(p => p.UserId != userId).ToList(); + + Participants.CurrentParticipants = remaining; + } + + /// + public void SetParticipantAsHost(int userId) + { + Debug.LogMessage(LogEventLevel.Debug, this, "SetParticipantAsHost: {0}", userId); + + foreach (var p in Participants.CurrentParticipants) + { + p.IsHost = p.UserId == userId; + } + + // Re-assign to trigger ParticipantsListHasChanged, since the mutations above happened + // on the existing list/items in place. + Participants.CurrentParticipants = Participants.CurrentParticipants; + } + + /// + public void AdmitParticipantFromWaitingRoom(int userId) + { + // No waiting room is simulated for this mock codec. + Debug.LogMessage(LogEventLevel.Debug, this, "AdmitParticipantFromWaitingRoom: {0} (no-op, mock has no waiting room)", userId); + } + + #endregion + void SetupCameras() { SupportsCameraAutoMode = true; @@ -884,20 +968,42 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec #region IRoutingSinkWithFeedback Members + /// + /// Gets the currently routed input port for this sink + /// public RoutingInputPort CurrentInputPort { get; private set; } + /// + /// Event fired when the currently routed input changes + /// public event InputChangedEventHandler InputChanged; #endregion #region ICurrentSources Members + /// + /// Gets the currently routed source devices, keyed by signal type + /// public Dictionary CurrentSources { get; private set; } = new Dictionary(); + /// + /// Gets the keys of the currently routed source devices, keyed by signal type + /// public Dictionary CurrentSourceKeys { get; private set; } = new Dictionary(); + /// + /// Event fired when the currently routed source for a signal type changes + /// public event EventHandler CurrentSourcesChanged; + /// + /// Sets the currently routed source device for the given signal type, updating + /// and and firing + /// + /// + /// The signal type being routed + /// The source device now routed to this signal type, or null if cleared public void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice) { CurrentSources.TryGetValue(signalType, out var previousSource); diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 6e7a1f5b..bc50316c 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -1273,9 +1273,13 @@ namespace PepperDash.Essentials.WebSocketServer Debug.LogMessage(LogEventLevel.Verbose, "Requesting Image: {0}", this, path); - var imageBasePath = Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo" + Global.DirectorySeparator; + // Served from the program's own slot-specific folder (e.g. /user/program2/logo/) via + // the Mobile Control direct server, rather than the processor's CWS /html/ webserver. + var imageBasePath = Global.FilePathPrefix + "logo" + Global.DirectorySeparator; - var image = path.Split('/').Last(); + // Path.GetFileName strips any directory components (including URL-decoded traversal + // sequences like "..") so only a bare filename within imageBasePath can ever be read. + var image = Path.GetFileName(Uri.UnescapeDataString(path.Split('?')[0])); var filePath = imageBasePath + image; From 4ab4ee168049e8b1bcbe37b39e42feb15df0cf2c Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Jul 2026 14:41:04 -0600 Subject: [PATCH 115/161] feat: update directory contact name from "Corporate Bridge" to "Overflow" --- .../VideoCodec/MockVC/MockCodecDirectory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockCodecDirectory.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockCodecDirectory.cs index e5699752..2e2f2653 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockCodecDirectory.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockCodecDirectory.cs @@ -107,7 +107,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec { new DirectoryContact() { - Name = "Corporate Bridge", + Name = "Overflow", ContactMethods = new List() { new ContactMethod() From 94837a60cf1dfc8c97b68ed4b4e77783cd0b8e22 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Jul 2026 15:37:14 -0600 Subject: [PATCH 116/161] feat: update debounce timer logic to handle multiple input ports for a single sink --- .../Routing/RoutingFeedbackManager.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index eac9b83a..4819de30 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -279,9 +279,15 @@ namespace PepperDash.Essentials.Core.Routing if (destination == null) return; - var key = destination.Key; + // Keyed by destination AND input port, not just destination: a single sink can have + // several independently-routed input ports (e.g. a codec with multiple simultaneous + // USB camera inputs, all downstream of the same midpoint). Keying by destination alone + // meant updates for one port would cancel/replace the still-pending debounce timer for + // a sibling port on the same sink, so only the last-processed port's update ever + // actually ran - the others silently never got refreshed. + var key = destination.Key + ":" + (inputPort?.Key ?? string.Empty); - // Cancel existing timer for this sink + // Cancel existing timer for this specific sink/port combination if (updateTimers.TryGetValue(key, out var existingTimer)) { existingTimer.Stop(); From d5432e489d8450628e8f2ff5385019f8e53e5def Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 8 Jul 2026 15:51:53 -0600 Subject: [PATCH 117/161] feat: add USB camera input ports for enhanced multi-camera routing support --- .../VideoCodec/MockVC/MockVC.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs index d728eef4..c3015a83 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs @@ -42,6 +42,25 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// Gets or sets the HdmiIn2 /// public RoutingInputPort HdmiIn2 { get; private set; } + + /// + /// Gets or sets the first USB camera input (usbIn1), matching the port key used by the + /// real Zoom Room plugin for simultaneous multi-camera routing. + /// + public RoutingInputPort UsbIn1 { get; private set; } + /// + /// Gets or sets the second USB camera input (usbIn2). + /// + public RoutingInputPort UsbIn2 { get; private set; } + /// + /// Gets or sets the third USB camera input (usbIn3). + /// + public RoutingInputPort UsbIn3 { get; private set; } + /// + /// Gets or sets the fourth USB camera input (usbIn4). + /// + public RoutingInputPort UsbIn4 { get; private set; } + /// /// Gets or sets the HdmiOut /// @@ -111,6 +130,18 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec InputPorts.Add(HdmiIn1); HdmiIn2 = new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, 2, this); InputPorts.Add(HdmiIn2); + // USB camera inputs - each participant's camera routes to one of these independently + // (unlike HdmiIn1/2, which represent a single selectable content source). Port keys + // match the real Zoom Room plugin ("usbIn1".."usbIn4") so demo/example configs that + // reference them resolve to an actual port instead of silently failing to find one. + UsbIn1 = new RoutingInputPort("usbIn1", eRoutingSignalType.Video, eRoutingPortConnectionType.UsbC, 3, this); + InputPorts.Add(UsbIn1); + UsbIn2 = new RoutingInputPort("usbIn2", eRoutingSignalType.Video, eRoutingPortConnectionType.UsbC, 4, this); + InputPorts.Add(UsbIn2); + UsbIn3 = new RoutingInputPort("usbIn3", eRoutingSignalType.Video, eRoutingPortConnectionType.UsbC, 5, this); + InputPorts.Add(UsbIn3); + UsbIn4 = new RoutingInputPort("usbIn4", eRoutingSignalType.Video, eRoutingPortConnectionType.UsbC, 6, this); + InputPorts.Add(UsbIn4); HdmiOut1 = new RoutingOutputPort(RoutingPortNames.HdmiOut1, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, null, this); OutputPorts.Add(HdmiOut1); HdmiOut2 = new RoutingOutputPort(RoutingPortNames.HdmiOut2, eRoutingSignalType.Audio | eRoutingSignalType.Video, eRoutingPortConnectionType.Hdmi, null, this); From f68e0338509cdaa9dba006d232fcacb44b7563f1 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 9 Jul 2026 09:33:59 -0600 Subject: [PATCH 118/161] feat(force-build): Rebuild with updated dev tools From 73bc50ef0d2c39eca119938fb1961fe6cca23449 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 9 Jul 2026 11:51:53 -0600 Subject: [PATCH 119/161] feat: update IP address retrieval logic to handle invalid values --- .../Web/RoutingFeedbackWebsocket.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs index 444a1e8c..5801116e 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -59,10 +59,10 @@ public class RoutingFeedbackWebsocket : IKeyed if (service == null) return ""; var ip = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1); - if (string.IsNullOrEmpty(ip)) + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + if (string.IsNullOrEmpty(ip) || ip == "Invalid Value") ip = CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1); return $"wss://{ip}:{_httpsServer.Port}{service.Path}"; } From f0025ce56cceb0a9bb681bf770d2e5315c1f78e4 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 8 Jul 2026 22:49:29 -0500 Subject: [PATCH 120/161] Backport PR #1439: packageManifest CWS endpoint + v1/v2 config merge fix Backports PepperDash/Essentials#1439 (merged to main at 6ceddcf5) onto dev/v3-routing (v3.0.0-dev-v3-routing.40): - New GET /packageManifest CWS route reporting Essentials/plugin/touchpanel UI package version info (GetPackageManifestRequestHandler). - EssentialsConfig.Versions now round-trips via [JsonProperty("versions")]. - ConfigReader now detects v1 vs v2 config by presence of system/template nodes (previously any versions node caused the v1->v2 merge to be skipped), and re-applies the versions node after merge since PortalConfigReader.MergeConfigs does not carry it forward. - Mobile control: track and validate UI client app versions (ConnectedClientVersionInfo, MobileControlSystemController changes merged cleanly from the PR). - Directory.Build.props: adds PackageId AssemblyMetadata item (kept this branch's 3.0.0-local version). Conflict resolution notes: - This branch had already introduced its own AppVersion type (Version + RepoUrl only) for TouchpanelWrapperApp/UserInterfaces. Replaced it with the PR's unified NugetVersion (adds PackageId/RepoUrl/Name, all NullValueHandling.Ignore) to match main's schema, since AppVersion had no other references in the codebase. - ConfigReader.LoadConfig on this branch had already fixed a double Stream.ReadToEnd() bug (reading fileContents once); kept that fix and layered the PR's v1/v2 detection fix + versions-node preservation on top of it, plus this branch's null-ConfigObject-after-merge guard. - EssentialsWebApi.SetupRoutes: inserted the new packageManifest route into this branch's existing (longer) route list, which already includes routing-specific endpoints not present on main. Builds clean (dotnet build on the full solution, 0 errors). Not pushed - pending local hardware/config testing before push. --- src/Directory.Build.props | 5 +- .../Config/Essentials/ConfigReader.cs | 21 +- .../Config/Essentials/EssentialsConfig.cs | 60 ++-- .../Web/EssentialsWebApi.cs | 5 + .../GetPackageManifestRequestHandler.cs | 259 ++++++++++++++++++ .../ConnectedClientVersionInfo.cs | 61 +++++ .../MobileControlSystemController.cs | 89 ++++++ 7 files changed, 467 insertions(+), 33 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs create mode 100644 src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ef8e86b0..bdc59cb9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ PepperDash Technology PepperDash Technology PepperDash Essentials - Copyright © 2025 + Copyright © 2026 https://github.com/PepperDash/Essentials git Crestron; 4series @@ -20,4 +20,7 @@ + + + diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index a1afba93..0dd39fee 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -132,12 +132,14 @@ namespace PepperDash.Essentials.Core.Config; { var parsedConfig = JObject.Parse(fileContents); - // 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["versions"] != null; + // A config is v1 if it has separate "system" and "template" nodes that + // need to be merged. A v2 config is already merged by the Portal API and + // will not have "system"/"template" nodes. This is independent of whether + // a "versions" node is present, which only carries version metadata and + // can appear on either a v1 or v2 config. + var isV1Config = parsedConfig["system"] != null && parsedConfig["template"] != null; - if (isV2Config) + if (!isV1Config) { Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary."); ConfigObject = parsedConfig.ToObject(); @@ -145,6 +147,8 @@ namespace PepperDash.Essentials.Core.Config; return ConfigObject != null; } + Debug.LogMessage(LogEventLevel.Information, "Config file is a v1 format, merging system and template."); + // Extract SystemUrl and TemplateUrl into final config output ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject(); @@ -163,6 +167,13 @@ namespace PepperDash.Essentials.Core.Config; { ConfigObject.TemplateUrl = parsedConfig["template_url"].Value(); } + + // MergeConfigs does not carry the "versions" node forward, so it must be + // applied separately to ensure it's preserved in the merged config. + if (parsedConfig["versions"] != null) + { + ConfigObject.Versions = parsedConfig["versions"].ToObject(); + } } Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config"); diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index 0fb0c93e..bcfd6227 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -16,9 +16,15 @@ namespace PepperDash.Essentials.Core.Config; /// public class EssentialsConfig : BasicConfig { + /// + /// Gets or sets the System URL for the Essentials configuration + /// [JsonProperty("system_url")] public string SystemUrl { get; set; } + /// + /// Gets or sets the Template URL for the Essentials configuration + /// [JsonProperty("template_url")] public string TemplateUrl { get; set; } @@ -74,14 +80,21 @@ public class EssentialsConfig : BasicConfig } } + /// + /// Gets or sets the list of rooms (device configurations) + /// [JsonProperty("rooms")] public List Rooms { get; set; } /// /// Gets or sets the Versions /// + [JsonProperty("versions")] public VersionData Versions { get; set; } + /// + /// Initializes a new instance of the class. + /// public EssentialsConfig() : base() { @@ -98,13 +111,13 @@ public class VersionData /// Gets or sets the Essentials version /// [JsonProperty("essentials")] - public NugetVersion Essentials { get; set; } + public PackageVersion Essentials { get; set; } /// /// Gets or sets the list of UserInterfaces /// [JsonProperty("userInterfaces")] - public List UserInterfaces { get; set; } + public List UserInterfaces { get; set; } /// /// Gets or sets the TouchpanelWrapperApp version @@ -112,45 +125,29 @@ public class VersionData /// to run an HTML5 user interface. /// [JsonProperty("touchpanelWrapperApp")] - public AppVersion TouchpanelWrapperApp { get; set; } + public PackageVersion TouchpanelWrapperApp { get; set; } /// /// Gets or sets the list of Packages /// [JsonProperty("packages")] - public List Packages { get; set; } + public List Packages { get; set; } /// /// Initializes a new instance of the class. /// public VersionData() { - UserInterfaces = new List(); - TouchpanelWrapperApp = new AppVersion(); - Packages = new List(); + UserInterfaces = new List(); + TouchpanelWrapperApp = new PackageVersion(); + Packages = new List(); } } -public class AppVersion -{ - /// - /// Gets or sets the Version - /// - [JsonProperty("version")] - public string Version { get; set; } - - /// - /// Gets or sets the RepoUrl - /// - [JsonProperty("repoUrl")] - public string RepoUrl { get; set; } -} - - /// -/// Represents a NugetVersion +/// Represents a Package Version /// -public class NugetVersion +public class PackageVersion { /// /// Gets or sets the Version @@ -161,14 +158,20 @@ public class NugetVersion /// /// Gets or sets the PackageId /// - [JsonProperty("packageId")] + [JsonProperty("packageId", NullValueHandling = NullValueHandling.Ignore)] public string PackageId { get; set; } /// /// Gets or sets the RepoUrl /// - [JsonProperty("repoUrl")] + [JsonProperty("repoUrl", NullValueHandling = NullValueHandling.Ignore)] public string RepoUrl { get; set; } + + /// + /// Gets or sets the human-readable name + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } } /// @@ -176,6 +179,9 @@ public class NugetVersion /// public class SystemTemplateConfigs { + /// + /// Gets or sets the System configuration + /// public EssentialsConfig System { get; set; } /// diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index 86f01803..9c16d47e 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -90,6 +90,11 @@ public class EssentialsWebApi : EssentialsDevice Name = "ReportVersions", RouteHandler = new ReportVersionsRequestHandler() }, + new HttpCwsRoute("packageManifest") + { + Name = "GetPackageManifest", + RouteHandler = new GetPackageManifestRequestHandler() + }, new HttpCwsRoute("appdebug") { Name = "AppDebug", diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs new file mode 100644 index 00000000..3d1b9eaa --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs @@ -0,0 +1,259 @@ +using System; +using System.Linq; +using System.Reflection; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core.Web.RequestHandlers; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers +{ + /// + /// Represents a GetPackageManifestRequestHandler + /// + public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler + { + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public GetPackageManifestRequestHandler() + : base(true) + { + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + try + { + var result = CloneVersionData(ConfigReader.ConfigObject?.Versions) ?? new VersionData(); + + PopulateEssentials(result); + PopulatePackages(result); + + var js = JsonConvert.SerializeObject(result, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); + } + catch (Exception ex) + { + PepperDash.Core.Debug.LogMessage(ex, "Exception handling GET /packageManifest request"); + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); + } + } + + /// + /// Deep-copies the config's VersionData so the live config object is never mutated + /// + private static VersionData CloneVersionData(VersionData source) + { + if (source == null) + { + return null; + } + + var json = JsonConvert.SerializeObject(source); + return JsonConvert.DeserializeObject(json); + } + + /// + /// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly + /// + private static void PopulateEssentials(VersionData result) + { + var essentials = result.Essentials ?? new PackageVersion(); + + essentials.Version = Global.AssemblyVersion; + + // The main program assembly (PackageId "PepperDashEssentials") is what's actually published + // to NuGet, but this handler lives in PepperDash.Essentials.Core, which can't reference that + // project's types directly (Essentials -> Core, not the reverse). PluginLoader.EssentialsAssembly.Assembly + // is unreliable (often left null - see PluginLoader.SetEssentialsAssembly), so look it up + // directly from the loaded AppDomain by its Directory.Build.props-embedded PackageId metadata + // (every project's .csproj sets its own PackageId explicitly), falling back to this handler's + // own (Core) assembly if it can't be found. + var essentialsAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => string.Equals(GetAssemblyMetadataValue(a, "PackageId"), "PepperDashEssentials", StringComparison.OrdinalIgnoreCase)) + ?? typeof(GetPackageManifestRequestHandler).Assembly; + + var repoUrl = TrimTrailingGit(GetAssemblyMetadataValue(essentialsAssembly, "RepositoryUrl")); + if (!string.IsNullOrEmpty(repoUrl)) + { + essentials.RepoUrl = repoUrl; + } + + var name = GetAssemblyProduct(essentialsAssembly); + if (!string.IsNullOrEmpty(name)) + { + essentials.Name = name; + } + + // Prefer the PackageId embedded via Directory.Build.props' AssemblyMetadata item (the + // authoritative source, matching the actual published PackageId) over any config-supplied + // or hardcoded value. + var reflectedPackageId = GetAssemblyMetadataValue(essentialsAssembly, "PackageId"); + if (!string.IsNullOrEmpty(reflectedPackageId)) + { + essentials.PackageId = reflectedPackageId; + } + else if (string.IsNullOrEmpty(essentials.PackageId)) + { + essentials.PackageId = "PepperDashEssentials"; + } + + result.Essentials = essentials; + } + + /// + /// Merges reflection data from loaded plugin assemblies with the config's packages list + /// + private static void PopulatePackages(VersionData result) + { + // Filter out null entries defensively - the packages list is deserialized from user-editable + // config JSON, so a malformed "packages": [null, ...] shouldn't throw and 500 the endpoint. + var configPackages = (result.Packages ?? new System.Collections.Generic.List()) + .Where(p => p != null) + .ToList(); + var matchedConfigPackages = new System.Collections.Generic.HashSet(); + var mergedPackages = new System.Collections.Generic.List(); + + foreach (var loaded in PluginLoader.EssentialsPluginAssemblies.Where(a => a.Assembly != null)) + { + var reflectedVersion = loaded.Version; + if (string.IsNullOrEmpty(reflectedVersion)) + { + // Never emit an entry with no version - the extension's parser drops entries + // whose version isn't a string. + continue; + } + + var reflectedRepoUrl = TrimTrailingGit(GetAssemblyMetadataValue(loaded.Assembly, "RepositoryUrl")); + var reflectedName = GetAssemblyProduct(loaded.Assembly); + + // Plugins built from a Directory.Build.props that embeds + // carry their PackageId + // directly - this is authoritative and should be preferred over the title/name fallback chain. + var reflectedPackageId = GetAssemblyMetadataValue(loaded.Assembly, "PackageId"); + + var assemblyTitle = GetAssemblyTitle(loaded.Assembly); + var assemblyName = loaded.Assembly.GetName().Name; + var assemblyNameNoSeriesSuffix = StripTrailingSeriesSuffix(assemblyName); + + var match = configPackages.FirstOrDefault(p => + !matchedConfigPackages.Contains(p) && + !string.IsNullOrEmpty(p.PackageId) && + (string.Equals(p.PackageId, reflectedPackageId, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.PackageId, assemblyTitle, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.PackageId, assemblyName, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.PackageId, assemblyNameNoSeriesSuffix, StringComparison.OrdinalIgnoreCase))); + + if (match != null) + { + matchedConfigPackages.Add(match); + + mergedPackages.Add(new PackageVersion + { + Name = !string.IsNullOrEmpty(match.Name) ? match.Name : reflectedName, + RepoUrl = !string.IsNullOrEmpty(match.RepoUrl) ? match.RepoUrl : reflectedRepoUrl, + PackageId = !string.IsNullOrEmpty(reflectedPackageId) ? reflectedPackageId : match.PackageId, + Version = reflectedVersion + }); + } + else + { + // Loaded but not present (or not matched) in config - emit the reflected PackageId + // when the assembly carries one, otherwise leave it null as before. + mergedPackages.Add(new PackageVersion + { + Name = reflectedName, + RepoUrl = reflectedRepoUrl, + PackageId = reflectedPackageId, + Version = reflectedVersion, + }); + } + } + + // Configured but not currently loaded - pass through unchanged + mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p))); + + result.Packages = mergedPackages; + } + + private static string GetAssemblyMetadataValue(Assembly assembly, string key) + { + if (assembly == null) + { + return null; + } + + var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false) + .Cast() + .FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase)); + + return match?.Value; + } + + private static string GetAssemblyProduct(Assembly assembly) + { + if (assembly == null) + { + return null; + } + + var attribute = assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false) + .FirstOrDefault() as AssemblyProductAttribute; + + return attribute?.Product; + } + + private static string GetAssemblyTitle(Assembly assembly) + { + if (assembly == null) + { + return null; + } + + var attribute = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false) + .FirstOrDefault() as AssemblyTitleAttribute; + + return attribute?.Title; + } + + private static string StripTrailingSeriesSuffix(string assemblyName) + { + const string suffix = ".4Series"; + + if (string.IsNullOrEmpty(assemblyName) || !assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + return assemblyName; + } + + return assemblyName.Substring(0, assemblyName.Length - suffix.Length); + } + + private static string TrimTrailingGit(string repoUrl) + { + const string suffix = ".git"; + + if (string.IsNullOrEmpty(repoUrl) || !repoUrl.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + return repoUrl; + } + + return repoUrl.Substring(0, repoUrl.Length - suffix.Length); + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs b/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs new file mode 100644 index 00000000..68f5e36e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs @@ -0,0 +1,61 @@ +using System; +using Newtonsoft.Json; + +namespace PepperDash.Essentials +{ + /// + /// Represents the version information reported by a connected Mobile Control UI client + /// + public class ConnectedClientVersionInfo + { + /// + /// Gets or sets the client id + /// + [JsonProperty("clientId")] + public string ClientId { get; set; } + + /// + /// Gets or sets the room key the client joined + /// + [JsonProperty("roomKey")] + public string RoomKey { get; set; } + + /// + /// Gets or sets the touchpanel key the client joined as, if any + /// + [JsonProperty("touchpanelKey")] + public string TouchpanelKey { get; set; } + + /// + /// Gets or sets the app version reported by the client (e.g. the React app's build-time APP_VERSION) + /// + [JsonProperty("appVersion")] + public string AppVersion { get; set; } + + /// + /// Gets or sets the expected app version from the system config's versions.touchpanelWrapperApp, if configured + /// + [JsonProperty("expectedAppVersion")] + public string ExpectedAppVersion { get; set; } + + /// + /// Gets or sets the UTC time the client last reported this version + /// + [JsonProperty("lastSeen")] + public DateTime LastSeen { get; set; } + + /// + /// Returns a copy of this instance, safe for callers outside the owning lock to hold/mutate + /// without affecting the internally tracked instance + /// + public ConnectedClientVersionInfo Clone() => new ConnectedClientVersionInfo + { + ClientId = ClientId, + RoomKey = RoomKey, + TouchpanelKey = TouchpanelKey, + AppVersion = AppVersion, + ExpectedAppVersion = ExpectedAppVersion, + LastSeen = LastSeen + }; + } +} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index f5c5f096..8de9d9eb 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -58,11 +58,32 @@ namespace PepperDash.Essentials private readonly Dictionary _defaultMessengers = new Dictionary(); + private readonly Dictionary _connectedClientVersions = + new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + private readonly object _connectedClientVersionsLock = new object(); + /// /// Get the custom messengers /// public ReadOnlyDictionary Messengers => new ReadOnlyDictionary(_messengers); + /// + /// Gets the most recently reported UI app version for each connected client, keyed by clientId + /// + public ReadOnlyDictionary ConnectedClientVersions + { + get + { + lock (_connectedClientVersionsLock) + { + return new ReadOnlyDictionary( + _connectedClientVersions.ToDictionary(kv => kv.Key, kv => kv.Value.Clone()) + ); + } + } + } + /// /// Get the default messengers /// @@ -1006,6 +1027,28 @@ namespace PepperDash.Essentials " Not Enabled in Config.\r\n" ); } + + var connectedClientVersions = ConnectedClientVersions; + + if (connectedClientVersions.Count == 0) + { + CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions: None reported yet\r\n"); + } + else + { + CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions:\r\n"); + foreach (var kv in connectedClientVersions) + { + var v = kv.Value; + var match = string.IsNullOrEmpty(v.ExpectedAppVersion) || string.Equals(v.ExpectedAppVersion, v.AppVersion, StringComparison.OrdinalIgnoreCase); + + CrestronConsole.ConsoleCommandResponse( + $" Client: {v.ClientId} Touchpanel: {v.TouchpanelKey} Room: {v.RoomKey}\r\n" + + $" Reported: {v.AppVersion} Expected: {(string.IsNullOrEmpty(v.ExpectedAppVersion) ? "(not configured)" : v.ExpectedAppVersion)} Match: {(match ? "Yes" : "NO - MISMATCH")}\r\n" + + $" Last Seen (UTC): {v.LastSeen:yyyy-MM-dd HH:mm:ss}\r\n" + ); + } + } } /// @@ -1402,6 +1445,8 @@ namespace PepperDash.Essentials var roomKey = content["roomKey"].Value(); var touchpanelKey = content.SelectToken("touchpanelKey"); + TrackClientAppVersion(clientId, roomKey, touchpanelKey?.Value(), content.SelectToken("appVersion")?.Value()); + if (_roomCombiner == null) { var message = new MobileControlMessage @@ -1473,6 +1518,50 @@ namespace PepperDash.Essentials SendTouchpanelKey(clientId, touchpanelKey); } + /// + /// Records the app version reported by a connecting UI client (e.g. the mobile control React app's + /// build-time APP_VERSION) and compares it against the configured versions.touchpanelWrapperApp version. + /// + private void TrackClientAppVersion(string clientId, string roomKey, string touchpanelKey, string appVersion) + { + if (string.IsNullOrEmpty(appVersion)) + { + return; + } + + var expectedVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version; + + var info = new ConnectedClientVersionInfo + { + ClientId = clientId, + RoomKey = roomKey, + TouchpanelKey = touchpanelKey, + AppVersion = appVersion, + ExpectedAppVersion = expectedVersion, + LastSeen = DateTime.UtcNow + }; + + lock (_connectedClientVersionsLock) + { + _connectedClientVersions[clientId] = info; + } + + if (!string.IsNullOrEmpty(expectedVersion) && !string.Equals(expectedVersion, appVersion, StringComparison.OrdinalIgnoreCase)) + { + this.LogWarning( + "Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}, which does not match configured versions.touchpanelWrapperApp version {expectedVersion}", + clientId, touchpanelKey, appVersion, expectedVersion + ); + } + else + { + this.LogVerbose( + "Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}", + clientId, touchpanelKey, appVersion + ); + } + } + private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken) { if (touchpanelKeyToken == null) From 2d8e42f566c185c0acf25c28d5f1daddbed8b885 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 8 Jul 2026 23:16:06 -0500 Subject: [PATCH 121/161] style: use file-scoped namespace in GetPackageManifestRequestHandler Matches the convention used by the other request handlers in this folder (ReportVersionsRequestHandler.cs, AppDebugRequestHandler.cs, etc). Addresses PR #1441 review comment. --- .../GetPackageManifestRequestHandler.cs | 481 +++++++++--------- 1 file changed, 240 insertions(+), 241 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs index 3d1b9eaa..284f3e2d 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs @@ -6,254 +6,253 @@ using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials.Core.Web.RequestHandlers +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Represents a GetPackageManifestRequestHandler +/// +public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler { /// - /// Represents a GetPackageManifestRequestHandler + /// Constructor /// - public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler + /// + /// base(true) enables CORS support by default + /// + public GetPackageManifestRequestHandler() + : base(true) { - /// - /// Constructor - /// - /// - /// base(true) enables CORS support by default - /// - public GetPackageManifestRequestHandler() - : base(true) + } + + /// + /// Handles GET method requests + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + try { + var result = CloneVersionData(ConfigReader.ConfigObject?.Versions) ?? new VersionData(); + + PopulateEssentials(result); + PopulatePackages(result); + + var js = JsonConvert.SerializeObject(result, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(js, false); + context.Response.End(); } - - /// - /// Handles GET method requests - /// - /// - protected override void HandleGet(HttpCwsContext context) + catch (Exception ex) { - try - { - var result = CloneVersionData(ConfigReader.ConfigObject?.Versions) ?? new VersionData(); - - PopulateEssentials(result); - PopulatePackages(result); - - var js = JsonConvert.SerializeObject(result, Formatting.Indented); - - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - context.Response.Write(js, false); - context.Response.End(); - } - catch (Exception ex) - { - PepperDash.Core.Debug.LogMessage(ex, "Exception handling GET /packageManifest request"); - context.Response.StatusCode = 500; - context.Response.StatusDescription = "Internal Server Error"; - context.Response.End(); - } - } - - /// - /// Deep-copies the config's VersionData so the live config object is never mutated - /// - private static VersionData CloneVersionData(VersionData source) - { - if (source == null) - { - return null; - } - - var json = JsonConvert.SerializeObject(source); - return JsonConvert.DeserializeObject(json); - } - - /// - /// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly - /// - private static void PopulateEssentials(VersionData result) - { - var essentials = result.Essentials ?? new PackageVersion(); - - essentials.Version = Global.AssemblyVersion; - - // The main program assembly (PackageId "PepperDashEssentials") is what's actually published - // to NuGet, but this handler lives in PepperDash.Essentials.Core, which can't reference that - // project's types directly (Essentials -> Core, not the reverse). PluginLoader.EssentialsAssembly.Assembly - // is unreliable (often left null - see PluginLoader.SetEssentialsAssembly), so look it up - // directly from the loaded AppDomain by its Directory.Build.props-embedded PackageId metadata - // (every project's .csproj sets its own PackageId explicitly), falling back to this handler's - // own (Core) assembly if it can't be found. - var essentialsAssembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => string.Equals(GetAssemblyMetadataValue(a, "PackageId"), "PepperDashEssentials", StringComparison.OrdinalIgnoreCase)) - ?? typeof(GetPackageManifestRequestHandler).Assembly; - - var repoUrl = TrimTrailingGit(GetAssemblyMetadataValue(essentialsAssembly, "RepositoryUrl")); - if (!string.IsNullOrEmpty(repoUrl)) - { - essentials.RepoUrl = repoUrl; - } - - var name = GetAssemblyProduct(essentialsAssembly); - if (!string.IsNullOrEmpty(name)) - { - essentials.Name = name; - } - - // Prefer the PackageId embedded via Directory.Build.props' AssemblyMetadata item (the - // authoritative source, matching the actual published PackageId) over any config-supplied - // or hardcoded value. - var reflectedPackageId = GetAssemblyMetadataValue(essentialsAssembly, "PackageId"); - if (!string.IsNullOrEmpty(reflectedPackageId)) - { - essentials.PackageId = reflectedPackageId; - } - else if (string.IsNullOrEmpty(essentials.PackageId)) - { - essentials.PackageId = "PepperDashEssentials"; - } - - result.Essentials = essentials; - } - - /// - /// Merges reflection data from loaded plugin assemblies with the config's packages list - /// - private static void PopulatePackages(VersionData result) - { - // Filter out null entries defensively - the packages list is deserialized from user-editable - // config JSON, so a malformed "packages": [null, ...] shouldn't throw and 500 the endpoint. - var configPackages = (result.Packages ?? new System.Collections.Generic.List()) - .Where(p => p != null) - .ToList(); - var matchedConfigPackages = new System.Collections.Generic.HashSet(); - var mergedPackages = new System.Collections.Generic.List(); - - foreach (var loaded in PluginLoader.EssentialsPluginAssemblies.Where(a => a.Assembly != null)) - { - var reflectedVersion = loaded.Version; - if (string.IsNullOrEmpty(reflectedVersion)) - { - // Never emit an entry with no version - the extension's parser drops entries - // whose version isn't a string. - continue; - } - - var reflectedRepoUrl = TrimTrailingGit(GetAssemblyMetadataValue(loaded.Assembly, "RepositoryUrl")); - var reflectedName = GetAssemblyProduct(loaded.Assembly); - - // Plugins built from a Directory.Build.props that embeds - // carry their PackageId - // directly - this is authoritative and should be preferred over the title/name fallback chain. - var reflectedPackageId = GetAssemblyMetadataValue(loaded.Assembly, "PackageId"); - - var assemblyTitle = GetAssemblyTitle(loaded.Assembly); - var assemblyName = loaded.Assembly.GetName().Name; - var assemblyNameNoSeriesSuffix = StripTrailingSeriesSuffix(assemblyName); - - var match = configPackages.FirstOrDefault(p => - !matchedConfigPackages.Contains(p) && - !string.IsNullOrEmpty(p.PackageId) && - (string.Equals(p.PackageId, reflectedPackageId, StringComparison.OrdinalIgnoreCase) || - string.Equals(p.PackageId, assemblyTitle, StringComparison.OrdinalIgnoreCase) || - string.Equals(p.PackageId, assemblyName, StringComparison.OrdinalIgnoreCase) || - string.Equals(p.PackageId, assemblyNameNoSeriesSuffix, StringComparison.OrdinalIgnoreCase))); - - if (match != null) - { - matchedConfigPackages.Add(match); - - mergedPackages.Add(new PackageVersion - { - Name = !string.IsNullOrEmpty(match.Name) ? match.Name : reflectedName, - RepoUrl = !string.IsNullOrEmpty(match.RepoUrl) ? match.RepoUrl : reflectedRepoUrl, - PackageId = !string.IsNullOrEmpty(reflectedPackageId) ? reflectedPackageId : match.PackageId, - Version = reflectedVersion - }); - } - else - { - // Loaded but not present (or not matched) in config - emit the reflected PackageId - // when the assembly carries one, otherwise leave it null as before. - mergedPackages.Add(new PackageVersion - { - Name = reflectedName, - RepoUrl = reflectedRepoUrl, - PackageId = reflectedPackageId, - Version = reflectedVersion, - }); - } - } - - // Configured but not currently loaded - pass through unchanged - mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p))); - - result.Packages = mergedPackages; - } - - private static string GetAssemblyMetadataValue(Assembly assembly, string key) - { - if (assembly == null) - { - return null; - } - - var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false) - .Cast() - .FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase)); - - return match?.Value; - } - - private static string GetAssemblyProduct(Assembly assembly) - { - if (assembly == null) - { - return null; - } - - var attribute = assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false) - .FirstOrDefault() as AssemblyProductAttribute; - - return attribute?.Product; - } - - private static string GetAssemblyTitle(Assembly assembly) - { - if (assembly == null) - { - return null; - } - - var attribute = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false) - .FirstOrDefault() as AssemblyTitleAttribute; - - return attribute?.Title; - } - - private static string StripTrailingSeriesSuffix(string assemblyName) - { - const string suffix = ".4Series"; - - if (string.IsNullOrEmpty(assemblyName) || !assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) - { - return assemblyName; - } - - return assemblyName.Substring(0, assemblyName.Length - suffix.Length); - } - - private static string TrimTrailingGit(string repoUrl) - { - const string suffix = ".git"; - - if (string.IsNullOrEmpty(repoUrl) || !repoUrl.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) - { - return repoUrl; - } - - return repoUrl.Substring(0, repoUrl.Length - suffix.Length); + PepperDash.Core.Debug.LogMessage(ex, "Exception handling GET /packageManifest request"); + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.End(); } } + + /// + /// Deep-copies the config's VersionData so the live config object is never mutated + /// + private static VersionData CloneVersionData(VersionData source) + { + if (source == null) + { + return null; + } + + var json = JsonConvert.SerializeObject(source); + return JsonConvert.DeserializeObject(json); + } + + /// + /// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly + /// + private static void PopulateEssentials(VersionData result) + { + var essentials = result.Essentials ?? new PackageVersion(); + + essentials.Version = Global.AssemblyVersion; + + // The main program assembly (PackageId "PepperDashEssentials") is what's actually published + // to NuGet, but this handler lives in PepperDash.Essentials.Core, which can't reference that + // project's types directly (Essentials -> Core, not the reverse). PluginLoader.EssentialsAssembly.Assembly + // is unreliable (often left null - see PluginLoader.SetEssentialsAssembly), so look it up + // directly from the loaded AppDomain by its Directory.Build.props-embedded PackageId metadata + // (every project's .csproj sets its own PackageId explicitly), falling back to this handler's + // own (Core) assembly if it can't be found. + var essentialsAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => string.Equals(GetAssemblyMetadataValue(a, "PackageId"), "PepperDashEssentials", StringComparison.OrdinalIgnoreCase)) + ?? typeof(GetPackageManifestRequestHandler).Assembly; + + var repoUrl = TrimTrailingGit(GetAssemblyMetadataValue(essentialsAssembly, "RepositoryUrl")); + if (!string.IsNullOrEmpty(repoUrl)) + { + essentials.RepoUrl = repoUrl; + } + + var name = GetAssemblyProduct(essentialsAssembly); + if (!string.IsNullOrEmpty(name)) + { + essentials.Name = name; + } + + // Prefer the PackageId embedded via Directory.Build.props' AssemblyMetadata item (the + // authoritative source, matching the actual published PackageId) over any config-supplied + // or hardcoded value. + var reflectedPackageId = GetAssemblyMetadataValue(essentialsAssembly, "PackageId"); + if (!string.IsNullOrEmpty(reflectedPackageId)) + { + essentials.PackageId = reflectedPackageId; + } + else if (string.IsNullOrEmpty(essentials.PackageId)) + { + essentials.PackageId = "PepperDashEssentials"; + } + + result.Essentials = essentials; + } + + /// + /// Merges reflection data from loaded plugin assemblies with the config's packages list + /// + private static void PopulatePackages(VersionData result) + { + // Filter out null entries defensively - the packages list is deserialized from user-editable + // config JSON, so a malformed "packages": [null, ...] shouldn't throw and 500 the endpoint. + var configPackages = (result.Packages ?? new System.Collections.Generic.List()) + .Where(p => p != null) + .ToList(); + var matchedConfigPackages = new System.Collections.Generic.HashSet(); + var mergedPackages = new System.Collections.Generic.List(); + + foreach (var loaded in PluginLoader.EssentialsPluginAssemblies.Where(a => a.Assembly != null)) + { + var reflectedVersion = loaded.Version; + if (string.IsNullOrEmpty(reflectedVersion)) + { + // Never emit an entry with no version - the extension's parser drops entries + // whose version isn't a string. + continue; + } + + var reflectedRepoUrl = TrimTrailingGit(GetAssemblyMetadataValue(loaded.Assembly, "RepositoryUrl")); + var reflectedName = GetAssemblyProduct(loaded.Assembly); + + // Plugins built from a Directory.Build.props that embeds + // carry their PackageId + // directly - this is authoritative and should be preferred over the title/name fallback chain. + var reflectedPackageId = GetAssemblyMetadataValue(loaded.Assembly, "PackageId"); + + var assemblyTitle = GetAssemblyTitle(loaded.Assembly); + var assemblyName = loaded.Assembly.GetName().Name; + var assemblyNameNoSeriesSuffix = StripTrailingSeriesSuffix(assemblyName); + + var match = configPackages.FirstOrDefault(p => + !matchedConfigPackages.Contains(p) && + !string.IsNullOrEmpty(p.PackageId) && + (string.Equals(p.PackageId, reflectedPackageId, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.PackageId, assemblyTitle, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.PackageId, assemblyName, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.PackageId, assemblyNameNoSeriesSuffix, StringComparison.OrdinalIgnoreCase))); + + if (match != null) + { + matchedConfigPackages.Add(match); + + mergedPackages.Add(new PackageVersion + { + Name = !string.IsNullOrEmpty(match.Name) ? match.Name : reflectedName, + RepoUrl = !string.IsNullOrEmpty(match.RepoUrl) ? match.RepoUrl : reflectedRepoUrl, + PackageId = !string.IsNullOrEmpty(reflectedPackageId) ? reflectedPackageId : match.PackageId, + Version = reflectedVersion + }); + } + else + { + // Loaded but not present (or not matched) in config - emit the reflected PackageId + // when the assembly carries one, otherwise leave it null as before. + mergedPackages.Add(new PackageVersion + { + Name = reflectedName, + RepoUrl = reflectedRepoUrl, + PackageId = reflectedPackageId, + Version = reflectedVersion, + }); + } + } + + // Configured but not currently loaded - pass through unchanged + mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p))); + + result.Packages = mergedPackages; + } + + private static string GetAssemblyMetadataValue(Assembly assembly, string key) + { + if (assembly == null) + { + return null; + } + + var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false) + .Cast() + .FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase)); + + return match?.Value; + } + + private static string GetAssemblyProduct(Assembly assembly) + { + if (assembly == null) + { + return null; + } + + var attribute = assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false) + .FirstOrDefault() as AssemblyProductAttribute; + + return attribute?.Product; + } + + private static string GetAssemblyTitle(Assembly assembly) + { + if (assembly == null) + { + return null; + } + + var attribute = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false) + .FirstOrDefault() as AssemblyTitleAttribute; + + return attribute?.Title; + } + + private static string StripTrailingSeriesSuffix(string assemblyName) + { + const string suffix = ".4Series"; + + if (string.IsNullOrEmpty(assemblyName) || !assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + return assemblyName; + } + + return assemblyName.Substring(0, assemblyName.Length - suffix.Length); + } + + private static string TrimTrailingGit(string repoUrl) + { + const string suffix = ".git"; + + if (string.IsNullOrEmpty(repoUrl) || !repoUrl.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + return repoUrl; + } + + return repoUrl.Substring(0, repoUrl.Length - suffix.Length); + } } From e7406dbeade8ba45a00aee83e59e03e7b08670e5 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Fri, 10 Jul 2026 09:51:23 -0500 Subject: [PATCH 122/161] ci(force-patch): increment patch by force From f0b546f97df2a764f2d98e1e9e6176b2c3dcd7fd Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 10 Jul 2026 10:05:49 -0600 Subject: [PATCH 123/161] feat: add custom logo URL feedback for light and dark backgrounds --- .../MobileControlTouchpanelController.cs | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs index 55d298d6..d640f22e 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using Crestron.SimplSharp; @@ -41,6 +42,25 @@ namespace PepperDash.Essentials.Touchpanel private readonly StringFeedback McServerUrlFeedback; private readonly StringFeedback UserCodeFeedback; + private string _customLogoUrlLightBkgnd; + private string _customLogoUrlDarkBkgnd; + + /// + /// Gets the CustomLogoUrlLightBkgndFeedback. Reports the absolute URL of the room's custom logo + /// meant to be displayed on a light background (as configured on the room device via + /// , served from the program's own slot-specific + /// logo folder), or an empty string if none is configured. + /// + public StringFeedback CustomLogoUrlLightBkgndFeedback { get; private set; } + + /// + /// Gets the CustomLogoUrlDarkBkgndFeedback. Reports the absolute URL of the room's custom logo + /// meant to be displayed on a dark background (as configured on the room device via + /// , served from the program's own slot-specific + /// logo folder), or an empty string if none is configured. + /// + public StringFeedback CustomLogoUrlDarkBkgndFeedback { get; private set; } + private readonly BoolFeedback _appOpenFeedback; /// @@ -135,6 +155,8 @@ namespace PepperDash.Essentials.Touchpanel QrCodeUrlFeedback = new StringFeedback($"{Key}-qrCodeUrl", () => _bridge?.QrCodeUrl); McServerUrlFeedback = new StringFeedback($"{Key}-mcServerUrl", () => _bridge?.McServerUrl); UserCodeFeedback = new StringFeedback($"{Key}-userCode", () => _bridge?.UserCode); + CustomLogoUrlLightBkgndFeedback = new StringFeedback($"{Key}-customLogoUrlLightBkgnd", () => _customLogoUrlLightBkgnd); + CustomLogoUrlDarkBkgndFeedback = new StringFeedback($"{Key}-customLogoUrlDarkBkgnd", () => _customLogoUrlDarkBkgnd); _appOpenFeedback = new BoolFeedback($"{Key}-appOpen", () => { @@ -185,7 +207,7 @@ namespace PepperDash.Essentials.Touchpanel Feedbacks = new FeedbackCollection { - AppUrlFeedback, QrCodeUrlFeedback, McServerUrlFeedback, UserCodeFeedback + AppUrlFeedback, QrCodeUrlFeedback, McServerUrlFeedback, UserCodeFeedback, CustomLogoUrlLightBkgndFeedback, CustomLogoUrlDarkBkgndFeedback }; ZoomFeedbacks = new FeedbackCollection { @@ -413,6 +435,8 @@ namespace PepperDash.Essentials.Touchpanel QrCodeUrlFeedback.LinkInputSig(Panel.StringInput[2]); McServerUrlFeedback.LinkInputSig(Panel.StringInput[3]); UserCodeFeedback.LinkInputSig(Panel.StringInput[4]); + CustomLogoUrlLightBkgndFeedback.LinkInputSig(Panel.StringInput[5]); + CustomLogoUrlDarkBkgndFeedback.LinkInputSig(Panel.StringInput[6]); Panel.IpInformationChange -= Panel_IpInformationChange; Panel.IpInformationChange += Panel_IpInformationChange; @@ -438,6 +462,8 @@ namespace PepperDash.Essentials.Touchpanel Panel.StringInput[2].StringValue = QrCodeUrlFeedback.StringValue; Panel.StringInput[3].StringValue = McServerUrlFeedback.StringValue; Panel.StringInput[4].StringValue = UserCodeFeedback.StringValue; + Panel.StringInput[5].StringValue = CustomLogoUrlLightBkgndFeedback.StringValue; + Panel.StringInput[6].StringValue = CustomLogoUrlDarkBkgndFeedback.StringValue; if (Panel is TswXX70Base x70Panel) { @@ -568,10 +594,57 @@ namespace PepperDash.Essentials.Touchpanel { this.LogInformation("AppURL changed: {appURL}", _bridge.AppUrl); SetAppUrl(_bridge.AppUrl); + UpdateCustomLogoUrls(); UpdateFeedbacks(s, a); }; SetAppUrl(_bridge.AppUrl); + UpdateCustomLogoUrls(); + } + + /// + /// Resolves the room's configured custom logos (light and dark background variants, if any) into + /// absolute URLs served from this processor's own slot-specific logo folder, and updates + /// CustomLogoUrlLightBkgndFeedback / CustomLogoUrlDarkBkgndFeedback accordingly. + /// + private void UpdateCustomLogoUrls() + { + var room = DeviceManager.AllDevices.OfType() + .FirstOrDefault(r => r.Key == _config.DefaultRoomKey); + + _customLogoUrlLightBkgnd = ResolveLogoUrl(room?.LogoUrlLightBkgnd); + _customLogoUrlDarkBkgnd = ResolveLogoUrl(room?.LogoUrlDarkBkgnd); + + CustomLogoUrlLightBkgndFeedback.FireUpdate(); + CustomLogoUrlDarkBkgndFeedback.FireUpdate(); + } + + /// + /// Resolves a room-configured logo file path into an absolute URL served from this processor's own + /// slot-specific logo folder (e.g. http://{ip}:{port}/mc/app/logo/{filename}). Returns an empty + /// string if is null/empty, or the app URL can't be determined. + /// + private string ResolveLogoUrl(string customLogoPath) + { + if (string.IsNullOrEmpty(customLogoPath) || string.IsNullOrEmpty(_bridge?.AppUrl)) + { + return string.Empty; + } + + var originMatch = Regex.Match(_bridge.AppUrl, @"^(https?://[^/]+)"); + + if (!originMatch.Success) + { + this.LogWarning("Could not determine origin from AppUrl '{appUrl}', cannot build custom logo URL", _bridge.AppUrl); + return string.Empty; + } + + // Path.GetFileName strips any directory components, matching the server's own image + // request handler, which only ever resolves a bare filename within its logo folder. + var filename = Path.GetFileName(customLogoPath); + var logoUrl = $"{originMatch.Groups[1].Value}/mc/app/logo/{filename}"; + + return GetUrlWithCorrectIp(logoUrl); } /// From 4631d3a5375728e23ddb49d93e5d612fe5ccbd85 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Fri, 10 Jul 2026 12:25:43 -0500 Subject: [PATCH 124/161] refactor: replace client-reported UI app version tracking with disk-based check Remove ConnectedClientVersionInfo and the per-client version dictionary in MobileControlSystemController, which grew unbounded and depended on a UI client connecting and self-reporting its APP_VERSION. Add TouchpanelWrapperAppVersionChecker, which instead scans the deployed mcUserApp files on disk for the configured versions.touchpanelWrapperApp version string. This works without any client connected and doesn't accumulate state. Wired into the mobileinfo console command output. --- .../ConnectedClientVersionInfo.cs | 61 -------- .../MobileControlSystemController.cs | 93 +----------- .../TouchpanelWrapperAppVersionChecker.cs | 141 ++++++++++++++++++ 3 files changed, 147 insertions(+), 148 deletions(-) delete mode 100644 src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs create mode 100644 src/PepperDash.Essentials.MobileControl/TouchpanelWrapperAppVersionChecker.cs diff --git a/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs b/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs deleted file mode 100644 index 68f5e36e..00000000 --- a/src/PepperDash.Essentials.MobileControl/ConnectedClientVersionInfo.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace PepperDash.Essentials -{ - /// - /// Represents the version information reported by a connected Mobile Control UI client - /// - public class ConnectedClientVersionInfo - { - /// - /// Gets or sets the client id - /// - [JsonProperty("clientId")] - public string ClientId { get; set; } - - /// - /// Gets or sets the room key the client joined - /// - [JsonProperty("roomKey")] - public string RoomKey { get; set; } - - /// - /// Gets or sets the touchpanel key the client joined as, if any - /// - [JsonProperty("touchpanelKey")] - public string TouchpanelKey { get; set; } - - /// - /// Gets or sets the app version reported by the client (e.g. the React app's build-time APP_VERSION) - /// - [JsonProperty("appVersion")] - public string AppVersion { get; set; } - - /// - /// Gets or sets the expected app version from the system config's versions.touchpanelWrapperApp, if configured - /// - [JsonProperty("expectedAppVersion")] - public string ExpectedAppVersion { get; set; } - - /// - /// Gets or sets the UTC time the client last reported this version - /// - [JsonProperty("lastSeen")] - public DateTime LastSeen { get; set; } - - /// - /// Returns a copy of this instance, safe for callers outside the owning lock to hold/mutate - /// without affecting the internally tracked instance - /// - public ConnectedClientVersionInfo Clone() => new ConnectedClientVersionInfo - { - ClientId = ClientId, - RoomKey = RoomKey, - TouchpanelKey = TouchpanelKey, - AppVersion = AppVersion, - ExpectedAppVersion = ExpectedAppVersion, - LastSeen = LastSeen - }; - } -} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 8de9d9eb..ea3b02d1 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -58,32 +58,11 @@ namespace PepperDash.Essentials private readonly Dictionary _defaultMessengers = new Dictionary(); - private readonly Dictionary _connectedClientVersions = - new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - private readonly object _connectedClientVersionsLock = new object(); - /// /// Get the custom messengers /// public ReadOnlyDictionary Messengers => new ReadOnlyDictionary(_messengers); - /// - /// Gets the most recently reported UI app version for each connected client, keyed by clientId - /// - public ReadOnlyDictionary ConnectedClientVersions - { - get - { - lock (_connectedClientVersionsLock) - { - return new ReadOnlyDictionary( - _connectedClientVersions.ToDictionary(kv => kv.Key, kv => kv.Value.Clone()) - ); - } - } - } - /// /// Get the default messengers /// @@ -1028,27 +1007,13 @@ namespace PepperDash.Essentials ); } - var connectedClientVersions = ConnectedClientVersions; + var expectedAppVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version; + var userAppPath = Global.FilePathPrefix + "mcUserApp" + Global.DirectorySeparator; + var versionCheck = TouchpanelWrapperAppVersionChecker.CheckDeployedVersion(userAppPath, expectedAppVersion); - if (connectedClientVersions.Count == 0) - { - CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions: None reported yet\r\n"); - } - else - { - CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions:\r\n"); - foreach (var kv in connectedClientVersions) - { - var v = kv.Value; - var match = string.IsNullOrEmpty(v.ExpectedAppVersion) || string.Equals(v.ExpectedAppVersion, v.AppVersion, StringComparison.OrdinalIgnoreCase); - - CrestronConsole.ConsoleCommandResponse( - $" Client: {v.ClientId} Touchpanel: {v.TouchpanelKey} Room: {v.RoomKey}\r\n" + - $" Reported: {v.AppVersion} Expected: {(string.IsNullOrEmpty(v.ExpectedAppVersion) ? "(not configured)" : v.ExpectedAppVersion)} Match: {(match ? "Yes" : "NO - MISMATCH")}\r\n" + - $" Last Seen (UTC): {v.LastSeen:yyyy-MM-dd HH:mm:ss}\r\n" - ); - } - } + CrestronConsole.ConsoleCommandResponse( + $"\r\nUI Wrapper App Deployed Version Check:\r\n {versionCheck.Summary}\r\n" + ); } /// @@ -1445,8 +1410,6 @@ namespace PepperDash.Essentials var roomKey = content["roomKey"].Value(); var touchpanelKey = content.SelectToken("touchpanelKey"); - TrackClientAppVersion(clientId, roomKey, touchpanelKey?.Value(), content.SelectToken("appVersion")?.Value()); - if (_roomCombiner == null) { var message = new MobileControlMessage @@ -1518,50 +1481,6 @@ namespace PepperDash.Essentials SendTouchpanelKey(clientId, touchpanelKey); } - /// - /// Records the app version reported by a connecting UI client (e.g. the mobile control React app's - /// build-time APP_VERSION) and compares it against the configured versions.touchpanelWrapperApp version. - /// - private void TrackClientAppVersion(string clientId, string roomKey, string touchpanelKey, string appVersion) - { - if (string.IsNullOrEmpty(appVersion)) - { - return; - } - - var expectedVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version; - - var info = new ConnectedClientVersionInfo - { - ClientId = clientId, - RoomKey = roomKey, - TouchpanelKey = touchpanelKey, - AppVersion = appVersion, - ExpectedAppVersion = expectedVersion, - LastSeen = DateTime.UtcNow - }; - - lock (_connectedClientVersionsLock) - { - _connectedClientVersions[clientId] = info; - } - - if (!string.IsNullOrEmpty(expectedVersion) && !string.Equals(expectedVersion, appVersion, StringComparison.OrdinalIgnoreCase)) - { - this.LogWarning( - "Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}, which does not match configured versions.touchpanelWrapperApp version {expectedVersion}", - clientId, touchpanelKey, appVersion, expectedVersion - ); - } - else - { - this.LogVerbose( - "Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}", - clientId, touchpanelKey, appVersion - ); - } - } - private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken) { if (touchpanelKeyToken == null) diff --git a/src/PepperDash.Essentials.MobileControl/TouchpanelWrapperAppVersionChecker.cs b/src/PepperDash.Essentials.MobileControl/TouchpanelWrapperAppVersionChecker.cs new file mode 100644 index 00000000..1368b788 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/TouchpanelWrapperAppVersionChecker.cs @@ -0,0 +1,141 @@ +using System; +using System.IO; +using System.Linq; + +namespace PepperDash.Essentials +{ + /// + /// Determines whether the touchpanel wrapper app (the mobile control React app) deployed to this + /// processor's mcUserApp folder matches the version configured in the system config's + /// versions.touchpanelWrapperApp. + /// + /// + /// This reads the app's built .js/.html files directly from disk, so unlike the previous + /// client-self-reported-version approach, it does not depend on any UI client being connected and + /// does not accumulate per-client state over the life of the program. + /// + public static class TouchpanelWrapperAppVersionChecker + { + private static readonly string[] SearchPatterns = { "*.js", "*.html" }; + + /// + /// Scans the deployed touchpanel wrapper app's .js/.html files under + /// for the literal string (the version build tooling bakes + /// into the app at build time). + /// + /// The path to the deployed mcUserApp folder + /// The expected version, from config's versions.touchpanelWrapperApp.version + public static TouchpanelWrapperAppVersionCheckResult CheckDeployedVersion(string appPath, string expectedVersion) + { + if (string.IsNullOrEmpty(expectedVersion)) + { + return TouchpanelWrapperAppVersionCheckResult.NotConfigured(appPath); + } + + if (string.IsNullOrEmpty(appPath) || !Directory.Exists(appPath)) + { + return TouchpanelWrapperAppVersionCheckResult.AppNotDeployed(appPath, expectedVersion); + } + + try + { + var files = SearchPatterns + .SelectMany(pattern => Directory.GetFiles(appPath, pattern, SearchOption.AllDirectories)) + .ToList(); + + if (files.Count == 0) + { + return TouchpanelWrapperAppVersionCheckResult.AppNotDeployed(appPath, expectedVersion); + } + + foreach (var file in files) + { + var contents = File.ReadAllText(file); + if (contents.IndexOf(expectedVersion, StringComparison.OrdinalIgnoreCase) >= 0) + { + return TouchpanelWrapperAppVersionCheckResult.Match(appPath, expectedVersion, file); + } + } + + return TouchpanelWrapperAppVersionCheckResult.Mismatch(appPath, expectedVersion, files.Count); + } + catch (Exception ex) + { + return TouchpanelWrapperAppVersionCheckResult.Error(appPath, expectedVersion, ex.Message); + } + } + } + + /// + /// The outcome of a check + /// + public class TouchpanelWrapperAppVersionCheckResult + { + /// + /// The mcUserApp path that was checked + /// + public string AppPath { get; } + + /// + /// The expected version from config, if any was configured + /// + public string ExpectedVersion { get; } + + /// + /// True if any files were found deployed at + /// + public bool AppDeployed { get; } + + /// + /// True if was found in one of the deployed files + /// + public bool VersionMatched { get; } + + /// + /// The file the expected version was found in, if is true + /// + public string MatchedFile { get; } + + /// + /// The number of files scanned + /// + public int FilesScanned { get; } + + /// + /// A human-readable summary of the outcome, suitable for console output + /// + public string Summary { get; } + + private TouchpanelWrapperAppVersionCheckResult(string appPath, string expectedVersion, bool appDeployed, + bool versionMatched, string matchedFile, int filesScanned, string summary) + { + AppPath = appPath; + ExpectedVersion = expectedVersion; + AppDeployed = appDeployed; + VersionMatched = versionMatched; + MatchedFile = matchedFile; + FilesScanned = filesScanned; + Summary = summary; + } + + internal static TouchpanelWrapperAppVersionCheckResult NotConfigured(string appPath) => + new TouchpanelWrapperAppVersionCheckResult(appPath, null, false, false, null, 0, + "versions.touchpanelWrapperApp.version is not configured; skipping check"); + + internal static TouchpanelWrapperAppVersionCheckResult AppNotDeployed(string appPath, string expectedVersion) => + new TouchpanelWrapperAppVersionCheckResult(appPath, expectedVersion, false, false, null, 0, + $"No app files found at '{appPath}'; expected version {expectedVersion}"); + + internal static TouchpanelWrapperAppVersionCheckResult Match(string appPath, string expectedVersion, string matchedFile) => + new TouchpanelWrapperAppVersionCheckResult(appPath, expectedVersion, true, true, matchedFile, 1, + $"Deployed app matches configured version {expectedVersion} (found in '{matchedFile}')"); + + internal static TouchpanelWrapperAppVersionCheckResult Mismatch(string appPath, string expectedVersion, int filesScanned) => + new TouchpanelWrapperAppVersionCheckResult(appPath, expectedVersion, true, false, null, filesScanned, + $"Deployed app at '{appPath}' does NOT contain expected version {expectedVersion} ({filesScanned} file(s) scanned) - MISMATCH"); + + internal static TouchpanelWrapperAppVersionCheckResult Error(string appPath, string expectedVersion, string errorMessage) => + new TouchpanelWrapperAppVersionCheckResult(appPath, expectedVersion, false, false, null, 0, + $"Error checking deployed app version at '{appPath}': {errorMessage}"); + } +} From 4a366a5a51ece5a4767939970a32b613b1ad4c95 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Fri, 10 Jul 2026 13:03:28 -0500 Subject: [PATCH 125/161] fix: guard against empty Version in configured-but-not-loaded package passthrough PopulatePackages' "configured but not currently loaded" pass-through could still emit PackageVersion entries with a null/empty Version, reintroducing the schema issue the loaded-reflection branch's own guard already prevents (downstream parsers drop entries whose version isn't a string). --- .../RequestHandlers/GetPackageManifestRequestHandler.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs index 284f3e2d..f89b0919 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs @@ -186,8 +186,11 @@ public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler } } - // Configured but not currently loaded - pass through unchanged - mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p))); + // Configured but not currently loaded - pass through unchanged, but keep the same + // "never emit an entry with no version" guarantee as the loaded-reflection branch above - + // otherwise this branch reintroduces the schema issue that guard is meant to prevent. + mergedPackages.AddRange(configPackages.Where(p => + !matchedConfigPackages.Contains(p) && !string.IsNullOrEmpty(p.Version))); result.Packages = mergedPackages; } From 3f8b97bff750fb0882354593ae51c8978145174d Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 10 Jul 2026 21:48:01 -0600 Subject: [PATCH 126/161] feat: add IHasDynamicMultiviewLayout interface and MultiviewParticipantSource class for dynamic layout support --- .../IHasDynamicMultiviewLayout.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasDynamicMultiviewLayout.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasDynamicMultiviewLayout.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasDynamicMultiviewLayout.cs new file mode 100644 index 00000000..786e2548 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasDynamicMultiviewLayout.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Defines a device (e.g. a multiview-capable video decoder) that can build a multiview tile +/// layout at runtime from a set of sources with priority values, rather than only supporting +/// pre-configured/named layouts. Implementing this interface (instead of requiring consumers to +/// reference the hardware-specific plugin type directly) lets other plugins (e.g. a room plugin) +/// drive dynamic, priority-based layouts on the device without taking a compile-time dependency on +/// the specific hardware plugin that implements it. +/// +public interface IHasDynamicMultiviewLayout +{ + /// + /// Computes and applies a multiview layout from the given participant sources (ordered by + /// priority) and an optional active presentation source. + /// + /// Sources to place in participant tiles, each with a priority (lower value = higher priority). + /// Device key for the active presentation source, or null/empty if no presentation is active. + /// True if the layout was successfully applied. + bool ApplyDynamicLayout(IReadOnlyList participantSources, string presentationSourceKey); +} + +/// +/// Represents a single source eligible for placement in a dynamic multiview layout, along with +/// its priority. Lower priority values are placed first / given more prominent tiles (i.e. lower +/// number = higher priority). +/// +public class MultiviewParticipantSource +{ + /// + /// The device key of the source to place in a tile. + /// + [JsonProperty("sourceKey")] + public string SourceKey { get; set; } + + /// + /// The priority of this source. Lower values are placed first / given more prominent tiles. + /// + [JsonProperty("priority")] + public int Priority { get; set; } + + /// + /// Parameterless constructor for deserialization. + /// + public MultiviewParticipantSource() + { + } + + /// + /// Initializes a new instance of the MultiviewParticipantSource class. + /// + public MultiviewParticipantSource(string sourceKey, int priority) + { + SourceKey = sourceKey; + Priority = priority; + } +} From 21995db550894717775193a49936c08c2875f7fb Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Jul 2026 10:40:56 -0600 Subject: [PATCH 127/161] feat: refactor FeedbackCollection and RoutingPortCollection to enforce unique keys and add AddRange method --- .../Feedbacks/FeedbackCollection.cs | 101 +++++++++++++++++- .../Routing/RoutingPortCollection.cs | 92 +++++++++++++++- 2 files changed, 183 insertions(+), 10 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs index 32b1abfb..aee11772 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackCollection.cs @@ -1,16 +1,27 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.ObjectModel; using Crestron.SimplSharp; namespace PepperDash.Essentials.Core; /// -/// Basically a List , with an indexer to find feedbacks by key name +/// Behaves like a List, with an indexer to find feedbacks by key name. Enforces unique feedback keys +/// (case-insensitive) - attempting to add a feedback whose key already exists in the collection throws an +/// . Feedbacks with no explicit key ( == "") are exempt +/// from the uniqueness check, since many feedbacks are intentionally constructed without a key and are not meant +/// to be looked up by the string indexer. /// -public class FeedbackCollection : List where T : Feedback +/// +/// Derives from rather than so key-uniqueness can be enforced +/// via the virtual Insert/Set/Remove/Clear hooks (List{T}'s members are not virtual). Public surface area +/// (int indexer, Add/Insert/Remove/Clear, collection-initializer support, enumeration) is preserved; an +/// method is provided for source compatibility with existing List{T}.AddRange call sites. +/// +public class FeedbackCollection : Collection where T : Feedback { + private readonly Dictionary _feedbacksByKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// /// Case-insensitive port lookup linked to feedbacks' keys /// @@ -18,7 +29,87 @@ public class FeedbackCollection : List where T : Feedback { get { - return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + return key != null && _feedbacksByKey.TryGetValue(key, out var feedback) ? feedback : null; + } + } + + /// + /// Adds a range of feedbacks to the collection. Provided for source compatibility with List{T}.AddRange. + /// + /// The feedbacks to add. + public void AddRange(IEnumerable items) + { + if (items == null) + throw new ArgumentNullException(nameof(items)); + + foreach (var item in items) + { + Add(item); + } + } + + /// + protected override void InsertItem(int index, T item) + { + ValidateNewItem(item); + base.InsertItem(index, item); + if (!string.IsNullOrEmpty(item.Key)) + { + _feedbacksByKey[item.Key] = item; + } + } + + /// + protected override void SetItem(int index, T item) + { + ValidateNewItem(item, indexBeingReplaced: index); + var replaced = Items[index]; + base.SetItem(index, item); + if (replaced != null && !string.IsNullOrEmpty(replaced.Key) && + !string.Equals(replaced.Key, item.Key, StringComparison.OrdinalIgnoreCase)) + { + _feedbacksByKey.Remove(replaced.Key); + } + if (!string.IsNullOrEmpty(item.Key)) + { + _feedbacksByKey[item.Key] = item; + } + } + + /// + protected override void RemoveItem(int index) + { + var removed = Items[index]; + base.RemoveItem(index); + if (removed != null && !string.IsNullOrEmpty(removed.Key)) + { + _feedbacksByKey.Remove(removed.Key); + } + } + + /// + protected override void ClearItems() + { + base.ClearItems(); + _feedbacksByKey.Clear(); + } + + private void ValidateNewItem(T item, int? indexBeingReplaced = null) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (string.IsNullOrEmpty(item.Key)) + return; + + if (_feedbacksByKey.TryGetValue(item.Key, out var existing)) + { + var existingIndex = Items.IndexOf(existing); + if (existingIndex != indexBeingReplaced) + { + throw new ArgumentException( + $"A Feedback with key '{item.Key}' already exists in this FeedbackCollection", nameof(item)); + } } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs b/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs index 736d507f..d78021b4 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingPortCollection.cs @@ -1,22 +1,104 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Collections.ObjectModel; namespace PepperDash.Essentials.Core; /// - /// Represents a RoutingPortCollection, which is essentially a List with an indexer for case-insensitive lookup of ports by their key names. + /// Represents a RoutingPortCollection, which behaves like a List with an indexer for case-insensitive lookup of + /// ports by their key names. Enforces unique port keys (case-insensitive) - attempting to add a port whose key + /// already exists in the collection throws an . /// - public class RoutingPortCollection : List where T: RoutingPort + /// + /// Derives from rather than so key-uniqueness can be enforced + /// via the virtual Insert/Set/Remove/Clear hooks (List{T}'s members are not virtual). Public surface area + /// (int indexer, Add/Insert/Remove/Clear, collection-initializer support, enumeration) is preserved; an + /// method is provided for source compatibility with existing List{T}.AddRange call sites. + /// + public class RoutingPortCollection : Collection where T: RoutingPort { + private readonly Dictionary _portsByKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// /// Case-insensitive port lookup linked to ports' keys /// - public T this[string key] + public T this[string key] { get { - return this.FirstOrDefault(i => i.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + return key != null && _portsByKey.TryGetValue(key, out var port) ? port : null; + } + } + + /// + /// Adds a range of ports to the collection. Provided for source compatibility with List{T}.AddRange. + /// + /// The ports to add. + public void AddRange(IEnumerable items) + { + if (items == null) + throw new ArgumentNullException(nameof(items)); + + foreach (var item in items) + { + Add(item); + } + } + + /// + protected override void InsertItem(int index, T item) + { + ValidateNewItem(item); + base.InsertItem(index, item); + _portsByKey[item.Key] = item; + } + + /// + protected override void SetItem(int index, T item) + { + ValidateNewItem(item, indexBeingReplaced: index); + var replaced = Items[index]; + base.SetItem(index, item); + if (replaced != null && !string.Equals(replaced.Key, item.Key, StringComparison.OrdinalIgnoreCase)) + { + _portsByKey.Remove(replaced.Key); + } + _portsByKey[item.Key] = item; + } + + /// + protected override void RemoveItem(int index) + { + var removed = Items[index]; + base.RemoveItem(index); + if (removed != null) + { + _portsByKey.Remove(removed.Key); + } + } + + /// + protected override void ClearItems() + { + base.ClearItems(); + _portsByKey.Clear(); + } + + private void ValidateNewItem(T item, int? indexBeingReplaced = null) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + if (string.IsNullOrEmpty(item.Key)) + throw new ArgumentException("RoutingPort must have a non-empty Key", nameof(item)); + + if (_portsByKey.TryGetValue(item.Key, out var existing)) + { + var existingIndex = Items.IndexOf(existing); + if (existingIndex != indexBeingReplaced) + { + throw new ArgumentException( + $"A RoutingPort with key '{item.Key}' already exists in this RoutingPortCollection", nameof(item)); + } } } } From 7d1e0432cbfc1ad533a72d8d3b78082d53096c2e Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 13 Jul 2026 12:26:57 -0600 Subject: [PATCH 128/161] feat: add IRoutingSinkWithLayouts interface to support layouts with multiple tile sinks --- .../Routing/IRoutingSinkWithLayouts.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayouts.cs diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayouts.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayouts.cs new file mode 100644 index 00000000..f0711df1 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayouts.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using PepperDash.Core; +using PepperDash.Essentials.Core.Routing; + +namespace PepperDash.Essentials.Core; + + +/// +/// Defines a routing sink (endpoint) device that supports layouts with multiple tile sinks. +/// Implements IRoutingSource so the windowed output of the layout can be routed to other devices. +/// +public interface IRoutingSinkWithLayouts : IRoutingSource +{ + /// + /// Gets the collection of window tile sinks for this routing sink with layouts. + /// Each tile sink represents a single window/tile in the layout and can be routed independently. + /// + Dictionary WindowTileSinks { get; } +} \ No newline at end of file From 079f2beb8bba9f2cf80a873b9615416af3e11eee Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 14 Jul 2026 19:34:14 -0600 Subject: [PATCH 129/161] feat: enhance routing graph handling for tile-sink children and add RoutingGraphHelpers utility class --- .../GetRoutingDevicesAndTieLinesHandler.cs | 188 ++++++++++++++++-- .../Web/RoutingFeedbackWebsocket.cs | 79 +++++--- .../Web/RoutingGraphHelpers.cs | 89 +++++++++ .../MobileControlWebsocketServer.cs | 23 ++- 4 files changed, 335 insertions(+), 44 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs index d532e7a2..0c5a3fae 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs @@ -5,6 +5,7 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; +using PepperDash.Essentials.Core.Web; namespace PepperDash.Essentials.Core.Web.RequestHandlers { @@ -27,9 +28,17 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers { var devices = new List(); + // Tile-sink children of an IRoutingSinkWithLayouts device (e.g. a multiview decoder's + // per-window sinks) are rendered as synthesized input ports on the parent's node instead + // of as their own separate nodes - see BuildRoutingDeviceInfo below. + var tileChildren = RoutingGraphHelpers.BuildTileChildMap(); + // Get all devices from DeviceManager foreach (var device in DeviceManager.AllDevices) { + if (tileChildren.ContainsKey(device.Key)) + continue; + var deviceInfo = new RoutingDeviceInfo { Key = device.Key, @@ -68,6 +77,27 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers deviceInfo.HasInputsAndOutputs = true; } + // Devices implementing IRoutingSinkWithLayouts (e.g. a multiview decoder) don't + // implement IRoutingInputs themselves - their tiles do. Synthesize one input port per + // tile (qualified so multiple tiles don't collide) so the graph can render a single + // node with one edge-target input per tile. + if (device is IRoutingSinkWithLayouts layoutDevice) + { + deviceInfo.HasInputs = true; + + var tilePorts = layoutDevice.WindowTileSinks + .OrderBy(kvp => kvp.Key) + .SelectMany(kvp => (kvp.Value as IRoutingInputs)?.InputPorts.Select(p => new PortInfo + { + Key = RoutingGraphHelpers.QualifyTilePortKey(kvp.Key, p.Key), + SignalType = p.Type.ToString(), + ConnectionType = p.ConnectionType.ToString(), + IsInternal = p.IsInternal + }) ?? []); + + deviceInfo.InputPorts = (deviceInfo.InputPorts ?? []).Concat(tilePorts).ToList(); + } + // Only include devices that have routing capabilities if (deviceInfo.HasInputs || deviceInfo.HasOutputs) { @@ -75,15 +105,28 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers } } - // Get all tielines - var tielines = TieLineCollection.Default.Select(tl => new TieLineInfo + // Get all tielines, remapping any that target a tile-sink child so they point at its + // IRoutingSinkWithLayouts parent's node/qualified port instead. + var tielines = TieLineCollection.Default.Select(tl => { - SourceDeviceKey = tl.SourcePort.ParentDevice.Key, - SourcePortKey = tl.SourcePort.Key, - DestinationDeviceKey = tl.DestinationPort.ParentDevice.Key, - DestinationPortKey = tl.DestinationPort.Key, - SignalType = tl.Type.ToString(), - IsInternal = tl.IsInternal + var destinationDeviceKey = tl.DestinationPort.ParentDevice.Key; + var destinationPortKey = tl.DestinationPort.Key; + + if (tileChildren.TryGetValue(destinationDeviceKey, out var tileInfo)) + { + destinationDeviceKey = tileInfo.Parent.Key; + destinationPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, destinationPortKey); + } + + return new TieLineInfo + { + SourceDeviceKey = tl.SourcePort.ParentDevice.Key, + SourcePortKey = tl.SourcePort.Key, + DestinationDeviceKey = destinationDeviceKey, + DestinationPortKey = destinationPortKey, + SignalType = tl.Type.ToString(), + IsInternal = tl.IsInternal + }; }).ToList(); // Get current active routes from DefaultCollection, grouped by signal type @@ -92,17 +135,43 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers .Select(g => new CurrentRouteGroupInfo { SignalType = g.Key, - Routes = [.. g.Select(d => new ActiveRouteInfo + Routes = [.. g.Select(d => { - SourceDeviceKey = d.Source.Key, - DestinationDeviceKey = d.Destination.Key, - DestinationInputPortKey = d.InputPort?.Key, - Steps = [.. d.Routes.Select(r => new RouteSwitchStepInfo + var destinationDeviceKey = d.Destination.Key; + var destinationInputPortKey = d.InputPort?.Key; + + if (tileChildren.TryGetValue(destinationDeviceKey, out var tileInfo)) { - SwitchingDeviceKey = r.SwitchingDevice?.Key, - InputPortKey = r.InputPort?.Key, - OutputPortKey = r.OutputPort?.Key - })] + destinationDeviceKey = tileInfo.Parent.Key; + if (destinationInputPortKey != null) + destinationInputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, destinationInputPortKey); + } + + return new ActiveRouteInfo + { + SourceDeviceKey = d.Source.Key, + DestinationDeviceKey = destinationDeviceKey, + DestinationInputPortKey = destinationInputPortKey, + Steps = [.. d.Routes.Select(r => + { + var switchingDeviceKey = r.SwitchingDevice?.Key; + var inputPortKey = r.InputPort?.Key; + + if (switchingDeviceKey != null && tileChildren.TryGetValue(switchingDeviceKey, out var stepTileInfo)) + { + switchingDeviceKey = stepTileInfo.Parent.Key; + if (inputPortKey != null) + inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(stepTileInfo.TileNumber, inputPortKey); + } + + return new RouteSwitchStepInfo + { + SwitchingDeviceKey = switchingDeviceKey, + InputPortKey = inputPortKey, + OutputPortKey = r.OutputPort?.Key + }; + })] + }; })] }).ToList(); @@ -110,7 +179,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers { Devices = devices, TieLines = tielines, - CurrentRoutes = currentRoutes + CurrentRoutes = currentRoutes, + SinkCurrentSources = BuildSinkCurrentSources(tileChildren) }; var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); @@ -122,6 +192,50 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers context.Response.Write(jsonResponse, false); context.Response.End(); } + + /// + /// Builds current-source info for every sink device, read directly from each sink's own + /// bookkeeping (see ). + /// Unlike -based , this + /// also reflects routes made via a device-specific bulk API (e.g. + /// IHasDynamicMultiviewLayout.ApplyDynamicLayout) that never creates a + /// or at all, so it's what the dev tools app + /// should use to seed initial sink-routing state on page load. + /// + private static List BuildSinkCurrentSources( + Dictionary tileChildren) + { + var result = new List(); + + foreach (var device in DeviceManager.AllDevices.OfType()) + { + if (device.CurrentInputPort == null) + continue; + + var sourceKey = RoutingGraphHelpers.GetCurrentSourceKey(device); + if (string.IsNullOrEmpty(sourceKey)) + continue; + + var deviceKey = device.Key; + var inputPortKey = device.CurrentInputPort.Key; + + if (tileChildren.TryGetValue(deviceKey, out var tileInfo)) + { + deviceKey = tileInfo.Parent.Key; + inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey); + } + + result.Add(new SinkCurrentSourceInfo + { + DeviceKey = deviceKey, + InputPortKey = inputPortKey, + SourceDeviceKey = sourceKey, + SignalType = device.CurrentInputPort.Type.ToString() + }); + } + + return result; + } } /// @@ -148,6 +262,44 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// [JsonProperty("currentRoutes")] public List CurrentRoutes { get; set; } + + /// + /// Gets or sets the current source feeding each sink device, read directly from each sink's own + /// current-source bookkeeping. Covers routes made via device-specific bulk APIs (e.g. + /// dynamic multiview layouts) that does not. + /// + [JsonProperty("sinkCurrentSources")] + public List SinkCurrentSources { get; set; } + } + + /// + /// Represents the source currently feeding a single sink device's input port + /// + public class SinkCurrentSourceInfo + { + /// + /// Gets or sets the key of the sink device (or its IRoutingSinkWithLayouts parent, if this is a tile) + /// + [JsonProperty("deviceKey")] + public string DeviceKey { get; set; } + + /// + /// Gets or sets the key of the input port currently receiving the source + /// + [JsonProperty("inputPortKey")] + public string InputPortKey { get; set; } + + /// + /// Gets or sets the key of the device currently feeding this input + /// + [JsonProperty("sourceDeviceKey")] + public string SourceDeviceKey { get; set; } + + /// + /// Gets or sets the signal type of the input port (e.g., AudioVideo, Audio, Video, etc.) + /// + [JsonProperty("signalType")] + public string SignalType { get; set; } } /// diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs index 5801116e..4e53e261 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -33,6 +33,11 @@ public class RoutingFeedbackWebsocket : IKeyed private readonly Dictionary _debounceTimers = new Dictionary(); + // Tile-sink children of an IRoutingSinkWithLayouts device (e.g. a multiview decoder's per-window + // sinks) are reported to clients as synthesized inputs on the parent's node, rather than as their + // own separate nodes - see RoutingGraphHelpers. Rebuilt whenever the server (re)starts. + private Dictionary _tileChildren = new Dictionary(); + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver(), @@ -157,7 +162,7 @@ public class RoutingFeedbackWebsocket : IKeyed internal string GetSnapshotMessage() { var midpointRoutes = new Dictionary>(); - var sinkRoutes = new Dictionary(); + var sinkRoutes = new Dictionary>(); // Collect midpoint current routes var midpointDevices = DeviceManager.AllDevices.OfType(); @@ -177,27 +182,42 @@ public class RoutingFeedbackWebsocket : IKeyed .ToList(); } - // Collect sink current sources + // Collect sink current sources directly from each sink's own current-source bookkeeping + // (ICurrentSources, part of IRoutingSinkWithFeedback), which is authoritative regardless of + // whether the route was made via a tie line (ReleaseAndMakeRoute) or a device-specific bulk + // API (e.g. IHasDynamicMultiviewLayout.ApplyDynamicLayout) that never touches + // TieLineCollection/RouteDescriptorCollection at all. var sinkDevices = DeviceManager.AllDevices.OfType(); foreach (var device in sinkDevices) { if (device.CurrentInputPort == null) continue; - // Trace back to find source - var tieLine = TieLineCollection.Default.FirstOrDefault(tl => - tl.DestinationPort.Key == device.CurrentInputPort.Key && - tl.DestinationPort.ParentDevice.Key == device.CurrentInputPort.ParentDevice.Key); + var sourceKey = RoutingGraphHelpers.GetCurrentSourceKey(device); + if (string.IsNullOrEmpty(sourceKey)) + continue; - if (tieLine != null) + var deviceKey = device.Key; + var inputPortKey = device.CurrentInputPort.Key; + + if (_tileChildren.TryGetValue(deviceKey, out var tileInfo)) { - sinkRoutes[device.Key] = new SinkRouteDto - { - InputPortKey = device.CurrentInputPort.Key, - SourceDeviceKey = tieLine.SourcePort.ParentDevice.Key, - SignalType = device.CurrentInputPort.Type.ToString() - }; + deviceKey = tileInfo.Parent.Key; + inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey); } + + if (!sinkRoutes.TryGetValue(deviceKey, out var routes)) + { + routes = new List(); + sinkRoutes[deviceKey] = routes; + } + + routes.Add(new SinkRouteDto + { + InputPortKey = inputPortKey, + SourceDeviceKey = sourceKey, + SignalType = device.CurrentInputPort.Type.ToString() + }); } var snapshot = new RoutingSnapshotDto @@ -212,6 +232,8 @@ public class RoutingFeedbackWebsocket : IKeyed private void SubscribeToRoutingEvents() { + _tileChildren = RoutingGraphHelpers.BuildTileChildMap(); + var midpointDevices = DeviceManager.AllDevices.OfType(); foreach (var device in midpointDevices) { @@ -267,22 +289,31 @@ public class RoutingFeedbackWebsocket : IKeyed private void HandleSinkInputChanged(IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort) { + // Tile-sink children are reported under their IRoutingSinkWithLayouts parent's key, with a + // qualified port key, so clients see this as an input change on the parent's node rather than + // on a device that isn't otherwise represented in the graph. + var deviceKey = sender.Key; + var inputPortKey = currentInputPort?.Key ?? ""; + + if (_tileChildren.TryGetValue(deviceKey, out var tileInfo)) + { + deviceKey = tileInfo.Parent.Key; + if (!string.IsNullOrEmpty(inputPortKey)) + inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey); + } + DebounceBroadcast($"sink-{sender.Key}", () => { - var sourceDeviceKey = ""; - if (currentInputPort != null) - { - var tieLine = TieLineCollection.Default.FirstOrDefault(tl => - tl.DestinationPort.Key == currentInputPort.Key && - tl.DestinationPort.ParentDevice.Key == currentInputPort.ParentDevice.Key); - sourceDeviceKey = tieLine?.SourcePort.ParentDevice.Key ?? ""; - } + // Read the source directly from the sink's own current-source bookkeeping (see + // GetCurrentSourceKey) rather than tracing a tie line - a route made via a + // device-specific bulk API (e.g. ApplyDynamicLayout) never creates a tie line at all. + var sourceDeviceKey = currentInputPort != null ? (RoutingGraphHelpers.GetCurrentSourceKey(sender) ?? "") : ""; var msg = new SinkInputChangedDto { Type = "sinkInputChanged", - DeviceKey = sender.Key, - InputPortKey = currentInputPort?.Key ?? "", + DeviceKey = deviceKey, + InputPortKey = inputPortKey, SourceDeviceKey = sourceDeviceKey, SignalType = currentInputPort?.Type.ToString() ?? "" }; @@ -360,7 +391,7 @@ public class RoutingFeedbackWebsocket : IKeyed { public string Type { get; set; } public Dictionary> MidpointRoutes { get; set; } - public Dictionary SinkRoutes { get; set; } + public Dictionary> SinkRoutes { get; set; } } private class MidpointRouteChangedDto diff --git a/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs b/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs new file mode 100644 index 00000000..714823ec --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using PepperDash.Core; + +namespace PepperDash.Essentials.Core.Web; + +/// +/// Shared helpers for building routing-graph data for consumers of the routing dev tools +/// ( and +/// ). +/// +/// +/// Devices that implement (e.g. a multiview decoder) expose a +/// set of per-tile child sink devices (), each of +/// which is independently registered with and independently routable. +/// Rendered naively, each tile shows up as its own top-level node in the routing graph, which is +/// confusing - a multiview decoder with N tiles doesn't look like N separate devices to a user. +/// These helpers let routing-graph consumers instead: skip tile-sink children when enumerating +/// top-level devices, and represent each tile as a distinctly-keyed input "edge target" on the +/// parent's own node (see ), remapping any tie line / active route +/// that targets a tile so it points at the parent device instead. +/// +public static class RoutingGraphHelpers +{ + /// + /// Describes where a tile-sink child device () + /// belongs, for remapping it back to its parent's node in routing-graph output. + /// + public readonly struct TileChildInfo + { + /// The parent device this tile belongs to. + public IRoutingSinkWithLayouts Parent { get; } + + /// The tile's 1-based window number within the parent's layout. + public int TileNumber { get; } + + /// Initializes a new instance of the struct. + public TileChildInfo(IRoutingSinkWithLayouts parent, int tileNumber) + { + Parent = parent; + TileNumber = tileNumber; + } + } + + /// + /// Builds a map of every tile-sink child device key (across all + /// devices currently in ) to its parent device and tile number. + /// + public static Dictionary BuildTileChildMap() + { + var map = new Dictionary(); + + foreach (var parent in DeviceManager.AllDevices.OfType()) + { + foreach (var kvp in parent.WindowTileSinks) + { + if (kvp.Value is not IKeyed tile || string.IsNullOrEmpty(tile.Key)) + continue; + + map[tile.Key] = new TileChildInfo(parent, kvp.Key); + } + } + + return map; + } + + /// + /// Builds a graph-unique port key for a tile's port, so multiple tiles synthesized onto the same + /// parent node don't collide (every tile's own InputPorts collection typically contains a + /// single, identically-keyed port, e.g. "tileInput"). + /// + public static string QualifyTilePortKey(int tileNumber, string portKey) => $"tile{tileNumber}:{portKey}"; + + /// + /// Gets the device key of whatever source is currently feeding a sink's Video signal (falling back + /// to Audio), read directly from its own bookkeeping (part of + /// ). This is authoritative regardless of whether the route + /// was made via a tie line (ReleaseAndMakeRoute) or a device-specific bulk API (e.g. + /// IHasDynamicMultiviewLayout.ApplyDynamicLayout) that never touches + /// TieLineCollection/RouteDescriptorCollection at all. + /// + public static string GetCurrentSourceKey(IRoutingSinkWithFeedback device) + { + if (device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Video, out var videoKey) && !string.IsNullOrEmpty(videoKey)) + return videoKey; + + return device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Audio, out var audioKey) ? audioKey : null; + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index bc50316c..ed5bdde8 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -1305,7 +1305,16 @@ namespace PepperDash.Essentials.WebSocketServer } byte[] contents = File.ReadAllBytes(filePath); res.ContentLength64 = contents.LongLength; - res.Close(contents, true); + try + { + res.Close(contents, true); + } + catch (IOException ex) + { + // Client disconnected (e.g. panel refreshed/navigated away) before the response could be + // fully written. This is a benign, expected condition, not an application error. + this.LogVerbose("Client disconnected before image response could be sent: {message}", ex.Message); + } } else { @@ -1427,7 +1436,17 @@ namespace PepperDash.Essentials.WebSocketServer } res.ContentLength64 = contents.LongLength; - res.Close(contents, true); + try + { + res.Close(contents, true); + } + catch (IOException ex) + { + // Client disconnected (e.g. panel refreshed/navigated away, or made a duplicate request) + // before the response could be fully written. This is a benign, expected condition + // (e.g. a "Broken pipe" IOException), not an application error worth logging at Error level. + this.LogVerbose("Client disconnected before user app response could be sent: {message}", ex.Message); + } } /// From f05a25c3e24d27628daed857c98f698dccd85eb4 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 14 Jul 2026 21:47:52 -0600 Subject: [PATCH 130/161] feat: update RoutingFeedbackManager to use ConcurrentDictionary for update timers to ensure thread safety --- .../Routing/RoutingFeedbackManager.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 4819de30..0ef974b9 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Timers; @@ -22,9 +23,13 @@ namespace PepperDash.Essentials.Core.Routing private Dictionary> midpointToSinkInputsMap; /// - /// Debounce timers for each sink device to prevent rapid successive updates + /// Debounce timers for each sink device to prevent rapid successive updates. Must be a + /// ConcurrentDictionary - it's mutated both from whatever thread calls UpdateDestination and + /// from each Timer's Elapsed callback (which fires on a threadpool timer thread), so a plain + /// Dictionary here would have its internal state corrupted by the concurrent Insert/Remove + /// calls, surfacing as an IndexOutOfRangeException from deep inside Dictionary internals. /// - private readonly Dictionary updateTimers = new Dictionary(); + private readonly ConcurrentDictionary updateTimers = new ConcurrentDictionary(); /// /// Debounce delay in milliseconds @@ -314,10 +319,9 @@ namespace PepperDash.Essentials.Core.Routing } finally { - if (updateTimers.ContainsKey(key)) + if (updateTimers.TryRemove(key, out var timerToDispose)) { - updateTimers[key]?.Dispose(); - updateTimers.Remove(key); + timerToDispose?.Dispose(); } } }; From 6852f47ce28ac328c5ba9ea82fae4b1149e14663 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 15 Jul 2026 11:35:51 -0600 Subject: [PATCH 131/161] feat: add multiview layout support with new interface and state classes --- .../Routing/IRoutingSinkWithLayoutState.cs | 46 ++++++++++ .../Routing/MultiviewLayoutState.cs | 85 +++++++++++++++++++ .../GetRoutingDevicesAndTieLinesHandler.cs | 13 ++- .../Web/RoutingFeedbackWebsocket.cs | 41 ++++++++- .../Web/RoutingGraphHelpers.cs | 23 +++++ 5 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayoutState.cs create mode 100644 src/PepperDash.Essentials.Core/Routing/MultiviewLayoutState.cs diff --git a/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayoutState.cs b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayoutState.cs new file mode 100644 index 00000000..a0ac9bec --- /dev/null +++ b/src/PepperDash.Essentials.Core/Routing/IRoutingSinkWithLayoutState.cs @@ -0,0 +1,46 @@ +using System; + +namespace PepperDash.Essentials.Core; + +/// +/// Extends for devices that can report the current shape of +/// their multiview canvas, along with the position/size, stacking order and routed source of every +/// tile, in a generic, product-agnostic form. This is intentionally decoupled from the routing +/// graph (nodes/edges/tie-lines) built from - +/// it exists to support a separate visualization concern: a mock-up of what is actually displayed +/// on the monitor fed by the decoder, suitable for a React UI or the developer tools Routing page. +/// +public interface IRoutingSinkWithLayoutState : IRoutingSinkWithLayouts +{ + /// + /// Gets the current multiview canvas/tile layout, or null if no layout is currently active or + /// applicable (e.g. the device is not currently in a multiview mode). + /// + MultiviewLayoutState CurrentLayout { get; } + + /// + /// Raised whenever the canvas shape, a tile's geometry/stacking order, or a tile's routed source + /// changes. + /// + event EventHandler LayoutChanged; +} + +/// +/// Event arguments for . +/// +public class MultiviewLayoutStateEventArgs : EventArgs +{ + /// + /// The current multiview layout state at the time this event was raised, or null if no layout + /// is currently active. + /// + public MultiviewLayoutState CurrentLayout { get; } + + /// + /// Initializes a new instance of the class. + /// + public MultiviewLayoutStateEventArgs(MultiviewLayoutState currentLayout) + { + CurrentLayout = currentLayout; + } +} diff --git a/src/PepperDash.Essentials.Core/Routing/MultiviewLayoutState.cs b/src/PepperDash.Essentials.Core/Routing/MultiviewLayoutState.cs new file mode 100644 index 00000000..382adebe --- /dev/null +++ b/src/PepperDash.Essentials.Core/Routing/MultiviewLayoutState.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PepperDash.Essentials.Core; + +/// +/// Describes the current shape of a multiview canvas and the position, size, stacking order and +/// routed source of every visible tile within it. Fully product-agnostic (no dependency on any +/// particular decoder/hardware type) and JSON-serializable so it can be sent as-is over the routing +/// feedback WebSocket () and the routing devices/tie-lines +/// HTTP snapshot (), for +/// rendering a visual mock-up of what is actually displayed on the fed monitor. +/// +public class MultiviewLayoutState +{ + /// + /// Width, in pixels, of the canvas that positions/sizes are expressed + /// against (typically the decoder's current output resolution width). + /// + [JsonProperty("canvasWidth")] + public int CanvasWidth { get; set; } + + /// + /// Height, in pixels, of the canvas that positions/sizes are expressed + /// against (typically the decoder's current output resolution height). + /// + [JsonProperty("canvasHeight")] + public int CanvasHeight { get; set; } + + /// + /// Position, size, stacking order and routed source for every visible tile in the layout. + /// + [JsonProperty("tiles")] + public List Tiles { get; set; } = new List(); +} + +/// +/// Describes a single tile/window within a . +/// +public class MultiviewTileState +{ + /// + /// 1-based tile/window number, matching the key in + /// . + /// + [JsonProperty("tileNumber")] + public int TileNumber { get; set; } + + /// + /// Device key of this tile's child sink, so a client can + /// cross-reference existing routing-feedback data (e.g. sink current-source state) for this tile + /// without re-deriving it. + /// + [JsonProperty("tileSinkKey")] + public string TileSinkKey { get; set; } + + /// Left edge of the tile, in pixels, within the canvas. + [JsonProperty("x")] + public int X { get; set; } + + /// Top edge of the tile, in pixels, within the canvas. + [JsonProperty("y")] + public int Y { get; set; } + + /// Width of the tile, in pixels. + [JsonProperty("width")] + public int Width { get; set; } + + /// Height of the tile, in pixels. + [JsonProperty("height")] + public int Height { get; set; } + + /// + /// Stacking order for overlapping tiles (e.g. picture-in-picture/overlay layouts). Tiles with + /// higher values are drawn on top of tiles with lower values. + /// + [JsonProperty("zOrder")] + public int ZOrder { get; set; } + + /// + /// Device key of the source currently routed to this tile, or null if the tile is empty. + /// + [JsonProperty("sourceDeviceKey")] + public string SourceDeviceKey { get; set; } +} diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs index 0c5a3fae..0c3e17e6 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs @@ -180,7 +180,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers Devices = devices, TieLines = tielines, CurrentRoutes = currentRoutes, - SinkCurrentSources = BuildSinkCurrentSources(tileChildren) + SinkCurrentSources = BuildSinkCurrentSources(tileChildren), + MultiviewLayouts = RoutingGraphHelpers.BuildMultiviewLayoutSnapshot() }; var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); @@ -270,6 +271,16 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// [JsonProperty("sinkCurrentSources")] public List SinkCurrentSources { get; set; } + + /// + /// Gets or sets the current multiview canvas/tile layout for every device implementing + /// , keyed by device key. Devices with no currently + /// active layout are omitted. Lets a client render an initial visual mock-up of each + /// multiview decoder's monitor output without waiting for a routing feedback WebSocket + /// connection - see . + /// + [JsonProperty("multiviewLayouts")] + public Dictionary MultiviewLayouts { get; set; } } /// diff --git a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs index 4e53e261..6cdf1e8e 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingFeedbackWebsocket.cs @@ -224,7 +224,8 @@ public class RoutingFeedbackWebsocket : IKeyed { Type = "snapshot", MidpointRoutes = midpointRoutes, - SinkRoutes = sinkRoutes + SinkRoutes = sinkRoutes, + Layouts = RoutingGraphHelpers.BuildMultiviewLayoutSnapshot() }; return JsonConvert.SerializeObject(snapshot, JsonSettings); @@ -245,6 +246,12 @@ public class RoutingFeedbackWebsocket : IKeyed { device.InputChanged += HandleSinkInputChanged; } + + var layoutDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in layoutDevices) + { + device.LayoutChanged += HandleLayoutChanged; + } } private void UnsubscribeFromRoutingEvents() @@ -260,6 +267,12 @@ public class RoutingFeedbackWebsocket : IKeyed { device.InputChanged -= HandleSinkInputChanged; } + + var layoutDevices = DeviceManager.AllDevices.OfType(); + foreach (var device in layoutDevices) + { + device.LayoutChanged -= HandleLayoutChanged; + } } private void HandleMidpointRouteChanged(IRoutingMidpointWithFeedback midpoint, RouteSwitchDescriptor newRoute) @@ -287,6 +300,24 @@ public class RoutingFeedbackWebsocket : IKeyed }); } + private void HandleLayoutChanged(object sender, MultiviewLayoutStateEventArgs e) + { + if (sender is not IKeyed device) + return; + + DebounceBroadcast($"layout-{device.Key}", () => + { + var msg = new LayoutChangedDto + { + Type = "layoutChanged", + DeviceKey = device.Key, + Layout = e.CurrentLayout + }; + + Broadcast(JsonConvert.SerializeObject(msg, JsonSettings)); + }); + } + private void HandleSinkInputChanged(IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort) { // Tile-sink children are reported under their IRoutingSinkWithLayouts parent's key, with a @@ -392,6 +423,14 @@ public class RoutingFeedbackWebsocket : IKeyed public string Type { get; set; } public Dictionary> MidpointRoutes { get; set; } public Dictionary> SinkRoutes { get; set; } + public Dictionary Layouts { get; set; } + } + + private class LayoutChangedDto + { + public string Type { get; set; } + public string DeviceKey { get; set; } + public MultiviewLayoutState Layout { get; set; } } private class MidpointRouteChangedDto diff --git a/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs b/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs index 714823ec..387c8824 100644 --- a/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs +++ b/src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs @@ -86,4 +86,27 @@ public static class RoutingGraphHelpers return device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Audio, out var audioKey) ? audioKey : null; } + + /// + /// Builds a snapshot of the current for every device in + /// that implements , keyed + /// by device key. Devices with no currently active layout (CurrentLayout == null) are + /// omitted. Shared by (initial + /// HTTP snapshot) and (WebSocket snapshot on connect). + /// + public static Dictionary BuildMultiviewLayoutSnapshot() + { + var result = new Dictionary(); + + foreach (var device in DeviceManager.AllDevices.OfType()) + { + var layout = device.CurrentLayout; + if (layout == null) + continue; + + result[device.Key] = layout; + } + + return result; + } } From f971a3adff46ca7720d064dacad62cfd300e8c39 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 21 Jul 2026 11:21:22 -0600 Subject: [PATCH 132/161] feat: add IDspPresetsDirectRecall interface to define preset recall functionality --- src/PepperDash.Core/Comm/GenericSshClient.cs | 21 +++++++++++++++++++ .../Devices/IDspPresetsDirectRecall.cs | 18 ++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/Devices/IDspPresetsDirectRecall.cs diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index 9d7fe001..1ab382fd 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -306,6 +306,27 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR ReconnectTimer.Start(); } } + catch (System.Net.Sockets.SocketException e) + { + // SSH.NET only wraps socket failures in SshConnectionException once the SSH + // protocol handshake begins - a failure before that (e.g. DNS resolution failure, + // "Name or service not known", or connection refused) surfaces as a raw + // SocketException here instead. This is an expected/routine "device offline" + // condition (same as the SshConnectionException case above), so log it at Warning + // instead of falling through to the generic handler's LogException/Error-level + // stack trace dump. + this.LogWarning("Connection failure: Cannot reach {host} on {port} ({message})", Hostname, Port, e.Message); + + DisconnectLogged = true; + KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); + if (AutoReconnect) + { + this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); + ReconnectTimer.Stop(); + ReconnectTimer.Interval = AutoReconnectIntervalMs; + ReconnectTimer.Start(); + } + } catch (Exception e) { this.LogException(e, "Unhandled exception on connect"); diff --git a/src/PepperDash.Essentials.Core/Devices/IDspPresetsDirectRecall.cs b/src/PepperDash.Essentials.Core/Devices/IDspPresetsDirectRecall.cs new file mode 100644 index 00000000..1e8c0b5c --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/IDspPresetsDirectRecall.cs @@ -0,0 +1,18 @@ +using PepperDash.Core; +using System.Collections.Generic; + +namespace PepperDash.Essentials.Core; + + +/// +/// Defines the contract for IDspPresetsDirectRecall +/// +public interface IDspPresetsDirectRecall : IKeyed +{ + + /// + /// Recalls the preset by name + /// + /// + void RunPreset(string name); +} \ No newline at end of file From 9656e3c408274030510c0eac38bd6fdd05d53e04 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 21 Jul 2026 13:19:25 -0600 Subject: [PATCH 133/161] feat: add audio input port to GenericSink for enhanced routing capabilities --- .../Generic/GenericSink.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index 4b54cf2a..7e1c7297 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -33,8 +33,10 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithFeedback InputPorts = new RoutingPortCollection(); var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); - + var audioInputPort = new RoutingInputPort(RoutingPortNames.AnyAudioIn, eRoutingSignalType.Audio, eRoutingPortConnectionType.LineAudio, null, this); + InputPorts.Add(inputPort); + InputPorts.Add(audioInputPort); CurrentSources = new Dictionary { From 42b358862feda70316ef9e354a9c29d7cceba1f2 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 21 Jul 2026 15:16:54 -0600 Subject: [PATCH 134/161] feat: add DisableEcho property to GenericSshClient and integrate with CommFactory --- src/PepperDash.Core/Comm/GenericSshClient.cs | 16 +++++++++++++++- .../Comm and IR/CommFactory.cs | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index 1ab382fd..471d8a2c 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.Timers; using Crestron.SimplSharp; @@ -137,6 +138,11 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR private bool DisconnectLogged = false; + /// + /// When true, turns off echo for the SSH session + /// + public bool DisableEcho { get; set; } + /// /// Typical constructor. /// @@ -250,7 +256,15 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR try { Client.Connect(); - TheStream = Client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534); + + var modes = new Dictionary(); + + if (DisableEcho) + { + modes.Add(TerminalModes.ECHO, 0); + } + + TheStream = Client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes); if (TheStream.DataAvailable) { // empty the buffer if there is data diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs index 82c5ce6c..3ba5300c 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs @@ -68,6 +68,7 @@ namespace PepperDash.Essentials.Core; { var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password); ssh.AutoReconnect = c.AutoReconnect; + ssh.DisableEcho = c.DisableSshEcho; if(ssh.AutoReconnect) ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; comm = ssh; From 4252e9c8e6710aeba74bcece92a5a0413d67ceb0 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 22 Jul 2026 09:04:43 -0600 Subject: [PATCH 135/161] feat: add thread safety to error timer management in StatusMonitorBase --- .../Monitoring/StatusMonitorBase.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs b/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs index 381ac6ba..cf6962b6 100644 --- a/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs +++ b/src/PepperDash.Essentials.Core/Monitoring/StatusMonitorBase.cs @@ -87,6 +87,14 @@ namespace PepperDash.Essentials.Core long ErrorTime; Timer WarningTimer; Timer ErrorTimer; + + // Guards WarningTimer/ErrorTimer against concurrent access - StartErrorTimers/StopErrorTimers/ + // ResetErrorTimers can each be invoked from different threads (e.g. a socket data-received + // callback vs. a connection-status-changed callback), and without this lock a null-check followed + // by a dereference (e.g. in ResetErrorTimers) can race with StopErrorTimers nulling the field in + // between, throwing an unhandled NullReferenceException on a background thread that crashes the + // whole program. + private readonly object _timerLock = new object(); /// Constructor /// /// parent device @@ -154,6 +162,8 @@ namespace PepperDash.Essentials.Core /// protected void StartErrorTimers() { + lock (_timerLock) + { if (WarningTimer == null) { WarningTimer = new Timer(WarningTime) { AutoReset = false }; @@ -167,16 +177,20 @@ namespace PepperDash.Essentials.Core ErrorTimer.Start(); } } + } /// /// Stops the error timers /// protected void StopErrorTimers() { - if (WarningTimer != null) WarningTimer.Stop(); - if (ErrorTimer != null) ErrorTimer.Stop(); - WarningTimer = null; - ErrorTimer = null; + lock (_timerLock) + { + if (WarningTimer != null) WarningTimer.Stop(); + if (ErrorTimer != null) ErrorTimer.Stop(); + WarningTimer = null; + ErrorTimer = null; + } } /// @@ -184,6 +198,8 @@ namespace PepperDash.Essentials.Core /// protected void ResetErrorTimers() { + lock (_timerLock) + { if (WarningTimer != null) { WarningTimer.Stop(); @@ -196,6 +212,7 @@ namespace PepperDash.Essentials.Core ErrorTimer.Interval = ErrorTime; ErrorTimer.Start(); } + } } } } \ No newline at end of file From 830e207ea71e3f4448e2825e5ed94ceea83b5fa3 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 22 Jul 2026 11:43:50 -0600 Subject: [PATCH 136/161] fix: correct device key reference in LevelControl property --- src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs b/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs index 04f14ac9..a4ca71c0 100644 --- a/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/LevelControlListItem.cs @@ -20,7 +20,7 @@ public class LevelControlListItem : AudioControlListItemBase get { if (_levelControl == null) - _levelControl = DeviceManager.GetDeviceForKey(ParentDeviceKey) as IBasicVolumeWithFeedback; + _levelControl = DeviceManager.GetDeviceForKey(DeviceKey) as IBasicVolumeWithFeedback; return _levelControl; } } From 56208342c953e806186d1346cd8e1d688eea7528 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 22 Jul 2026 15:36:15 -0600 Subject: [PATCH 137/161] feat: implement deployment of mcUserApp zip files with cleanup and extraction --- .../MobileControlWebsocketServer.cs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index ed5bdde8..0f03f2a9 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Security.Authentication; @@ -428,6 +429,8 @@ namespace PepperDash.Essentials.WebSocketServer Directory.CreateDirectory(userAppPath); } + DeployMcUserAppZipIfPresent(); + if (!Directory.Exists($"{userAppPath}{localConfigFolderName}")) { Directory.CreateDirectory($"{userAppPath}{localConfigFolderName}"); @@ -481,6 +484,111 @@ namespace PepperDash.Essentials.WebSocketServer } } + /// + /// Checks the mcUserApp folder for a deployed zip bundle (e.g. dropped there directly by an + /// SFTP deploy script or manual upload) and, if found, wipes the folder's existing contents + /// (except the zip itself) and extracts the zip in its place. This allows a new touchpanel + /// wrapper app build to be deployed just by copying a zip file into the already-existing + /// mcUserApp folder, without needing to separately clear out the previous build first. + /// + private void DeployMcUserAppZipIfPresent() + { + string[] zipFiles; + + try + { + zipFiles = Directory.GetFiles(userAppPath, "*.zip", SearchOption.TopDirectoryOnly); + } + catch (Exception ex) + { + this.LogError("Error scanning {userAppPath} for a deployed mcUserApp zip file: {message}", userAppPath, ex.Message); + this.LogDebug(ex, "Stack Trace"); + return; + } + + if (zipFiles.Length == 0) + { + return; + } + + if (zipFiles.Length > 1) + { + this.LogWarning("Multiple zip files found in {userAppPath}. Using the first one found: {zipFile}", userAppPath, zipFiles[0]); + } + + var zipFilePath = zipFiles[0]; + + try + { + this.LogInformation("Found mcUserApp deployment zip file {zipFile}. Clearing existing app contents and unzipping...", zipFilePath); + + // Remove everything currently in the folder except the zip file itself, so the new + // build doesn't end up mixed in with stale files from a previous deployment. + foreach (var entryPath in Directory.GetFileSystemEntries(userAppPath)) + { + if (string.Equals(entryPath, zipFilePath, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (Directory.Exists(entryPath)) + { + Directory.Delete(entryPath, recursive: true); + } + else + { + File.Delete(entryPath); + } + } + + var appRoot = Path.GetFullPath(userAppPath); + if (!appRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && + !appRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + appRoot += Path.DirectorySeparatorChar; + } + + using (var archive = ZipFile.OpenRead(zipFilePath)) + { + foreach (var entry in archive.Entries) + { + var destinationPath = Path.Combine(userAppPath, entry.FullName); + var fullDest = Path.GetFullPath(destinationPath); + + // Guard against "zip slip" - a malicious/corrupt archive entry trying to + // extract outside of the mcUserApp folder via ../ path segments. + if (!fullDest.StartsWith(appRoot, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + } + + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(destinationPath); + continue; + } + + var parentDir = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(parentDir)) + { + Directory.CreateDirectory(parentDir); + } + + entry.ExtractToFile(destinationPath, overwrite: true); + } + } + + File.Delete(zipFilePath); + + this.LogInformation("Successfully deployed mcUserApp from {zipFile}", Path.GetFileName(zipFilePath)); + } + catch (Exception ex) + { + this.LogError("Error deploying mcUserApp zip file {zipFile}: {message}", zipFilePath, ex.Message); + this.LogDebug(ex, "Stack Trace"); + } + } + private MobileControlApplicationConfig GetApplicationConfig(string processorIp) { try From dac5325ecff3faffe12a59a2cf9b559e49b1d3e6 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 23 Jul 2026 08:47:57 -0600 Subject: [PATCH 138/161] feat: add IDtmfDecode interface and DtmfReceivedEventArgs class for DTMF digit handling --- .../AudioCodec/Interfaces/IDtmfDecode.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs new file mode 100644 index 00000000..68c06bfd --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs @@ -0,0 +1,19 @@ +namespace PepperDash.Essentials.Devices.Common.AudioCodec; + +public interface IDtmfDecode +{ + /// + /// Event fired when a DTMF digit is received + /// + event EventHandler DtmfReceived; +} + +public class DtmfReceivedEventArgs : EventArgs +{ + public string Digit { get; private set; } + + public DtmfReceivedEventArgs(string digit) + { + Digit = digit; + } +} \ No newline at end of file From a782651e4d950cb3e26d340aa551e6131ee4cd6a Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 23 Jul 2026 08:49:11 -0600 Subject: [PATCH 139/161] feat: enhance documentation for IDtmfDecode interface and DtmfReceivedEventArgs class --- .../AudioCodec/Interfaces/IDtmfDecode.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs index 68c06bfd..27f8ee3a 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs @@ -1,5 +1,8 @@ namespace PepperDash.Essentials.Devices.Common.AudioCodec; +/// +/// Interface for devices that can decode DTMF digits +/// public interface IDtmfDecode { /// @@ -8,10 +11,20 @@ public interface IDtmfDecode event EventHandler DtmfReceived; } +/// +/// Event args for DTMF received event +/// public class DtmfReceivedEventArgs : EventArgs { + /// + /// The DTMF digit that was received + /// public string Digit { get; private set; } + /// + /// Event args for DTMF received event + /// + /// The DTMF digit that was received public DtmfReceivedEventArgs(string digit) { Digit = digit; From 5e0a4c04ad925234174e7eae0d467280225236ee Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 23 Jul 2026 08:54:53 -0600 Subject: [PATCH 140/161] feat: add missing using directive for System in IDtmfDecode interface --- .../AudioCodec/Interfaces/IDtmfDecode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs index 27f8ee3a..a161a3d1 100644 --- a/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs +++ b/src/PepperDash.Essentials.Devices.Common/AudioCodec/Interfaces/IDtmfDecode.cs @@ -1,3 +1,4 @@ +using System; namespace PepperDash.Essentials.Devices.Common.AudioCodec; /// From 5a183e733394177a9a42dd9810e7ac189ff36d26 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 23 Jul 2026 13:00:36 -0600 Subject: [PATCH 141/161] (force-patch): Updates devtools version From 61b5ca6297591d40edb3d375915ab0d5bb09167f Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 23 Jul 2026 22:16:55 -0600 Subject: [PATCH 142/161] feat: add Mobile Control documentation and update table of contents --- .../Connection-Based-Routing.md | 116 ++++++++- docs/docs/technical-docs/Mobile-Control.md | 240 ++++++++++++++++++ docs/docs/toc.yml | 2 + 3 files changed, 349 insertions(+), 9 deletions(-) create mode 100644 docs/docs/technical-docs/Mobile-Control.md diff --git a/docs/docs/technical-docs/Connection-Based-Routing.md b/docs/docs/technical-docs/Connection-Based-Routing.md index 4ba902db..f5ff3be0 100644 --- a/docs/docs/technical-docs/Connection-Based-Routing.md +++ b/docs/docs/technical-docs/Connection-Based-Routing.md @@ -12,12 +12,18 @@ When routes are to be executed, Essentials will use this connection graph to dec ### Classes Referenced -* `PepperDash.Essentials.Core.Routing.IRoutingSource` -* `PepperDash.Essentials.Core.Routing.IRoutingOutputs` -* `PepperDash.Essentials.Core.Routing.IRoutingInputs` -* `PepperDash.Essentials.Core.Routing.IRoutingInputsOutputs` -* `PepperDash.Essentials.Core.Routing.IRoutingSinkNoSwitching` -* `PepperDash.Essentials.Core.Routing.IRoutingSinkWithSwitching` +* `PepperDash.Essentials.Core.IRoutingSource` +* `PepperDash.Essentials.Core.IRoutingOutputs` +* `PepperDash.Essentials.Core.IRoutingInputs` +* `PepperDash.Essentials.Core.IRoutingMidpoint` +* `PepperDash.Essentials.Core.IRoutingMidpointWithFeedback` +* `PepperDash.Essentials.Core.IRoutingSinkWithFeedback` + +> **Note:** As of the v3 routing refactor, the older `IRoutingInputsOutputs`, `IRouting`, +> `IRoutingWithFeedback`, `IRoutingWithClear`, `IRoutingSink`, `IRoutingSinkWithInputPort`, +> `IRoutingSinkNoSwitching`, `IRoutingSinkWithSwitching`, and `IRoutingSinkWithSwitchingWithInputPort` +> interfaces have been removed and consolidated - see [Routing interfaces (v3)](#routing-interfaces-v3) +> below for the current mapping. ## Example system, a simple presentation system @@ -56,11 +62,12 @@ Source devices in Essentials must implement `IRoutingOutputs` or `IRoutingSource A midpoint is a device in the middle of the signal chain. Typically a switcher, matrix or otherwise. Examples: DM chassis; DM-TX; DM-RMC; A video codec. These devices will have input and output ports. -Midpoint devices must implement `IRoutingInputsOutputs`. Midpoints with switching must implement `IRouting`. +Passthrough midpoints (no active switching) must implement `IRoutingMidpoint`. Midpoints that actively +switch and report their current routes must implement `IRoutingMidpointWithFeedback`. #### Sink -A sink is a device at the end of a full signal path. For example, a display, amplifier, encoder, etc. Sinks typically contain only input ports. They may or may not have switching, like a display with several inputs. Classes defining sink devices must implement `IRoutingSinkNoSwitching` or `IRoutingSinkWithSwitching`. +A sink is a device at the end of a full signal path. For example, a display, amplifier, encoder, etc. Sinks typically contain only input ports. They may or may not have switching, like a display with several inputs. Classes defining sink devices must implement `IRoutingSinkWithFeedback`. #### Tie-line @@ -78,4 +85,95 @@ A tie-line is a logical representation of a physical cable connection between tw ### Interfaces -Todo: Define Interfaces IRouting, IRoutingOutputs, IRoutingInputs +#### Routing interfaces (v3) + +The v3 routing refactor consolidated several older, overlapping interfaces into two feedback-aware +interfaces. If you're updating a device implemented against the pre-v3 API, use this table to find +its replacement: + +| Old interface (pre-v3, removed) | Current interface | +| --- | --- | +| `IRoutingInputsOutputs` | `IRoutingMidpoint` | +| `IRouting`, `IRoutingWithFeedback`, `IRoutingWithClear` | `IRoutingMidpointWithFeedback` | +| `IRoutingSink`, `IRoutingSinkWithInputPort`, `IRoutingSinkWithSwitching`, `IRoutingSinkWithSwitchingWithInputPort` | `IRoutingSinkWithFeedback` | + +**Base building-block interfaces** (unchanged): + +* `IRoutingSource` - marker interface for a device that originates a signal path; extends `IRoutingOutputs`. +* `IRoutingOutputs` - exposes `RoutingPortCollection OutputPorts`. +* `IRoutingInputs` - exposes `RoutingPortCollection InputPorts`. + +**Midpoint interfaces:** + +* `IRoutingMidpoint` - a passthrough device with both input and output ports but no active switching. Extends `IRoutingInputs` and `IRoutingOutputs`. +* `IRoutingMidpointWithFeedback` - a midpoint that actively switches and reports feedback. Extends `IRoutingMidpoint` and adds: + * `void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType)` + * `void ClearRoute(object outputSelector, eRoutingSignalType signalType)` + * `List CurrentRoutes { get; }` + * `event RouteChangedEventHandler RouteChanged` + +**Sink interface:** + +* `IRoutingSinkWithFeedback` - a routing endpoint device that can switch inputs and provides feedback. Extends `IRoutingInputs`, `IKeyName`, and `ICurrentSources` (see below), and adds: + * `void ExecuteSwitch(object inputSelector)` + * `RoutingInputPort CurrentInputPort { get; }` + * `event InputChangedEventHandler InputChanged` + +#### Current source tracking (`ICurrentSources`) + +`ICurrentSources` is a new interface (implemented by `IRoutingSinkWithFeedback`) that reports which +source is actually feeding a sink right now, per signal type: + +* `Dictionary CurrentSources { get; }` +* `Dictionary CurrentSourceKeys { get; }` +* `event EventHandler CurrentSourcesChanged` + +A new `RoutingFeedbackManager` subscribes to `RouteChanged` on every `IRoutingMidpointWithFeedback` +and `InputChanged` on every `IRoutingSinkWithFeedback` in the system, traces each change back through +the tie-line graph to the originating source (debounced ~500ms), and keeps each sink's +`ICurrentSources` state up to date automatically - developers no longer need to manually wire up +source tracking per device. + +#### Multiview layout support + +For decoders that can display several independently-routable tiles at once (e.g. a WyreStorm +NetworkHD-style multiview decoder), two additional interfaces are available: + +* `IRoutingSinkWithLayouts` - extends `IRoutingSource` and exposes `Dictionary WindowTileSinks`, one per-tile "virtual sink" that can be routed independently like any other `IRoutingSinkWithFeedback`. +* `IRoutingSinkWithLayoutState` - extends `IRoutingSinkWithLayouts` and additionally reports the current canvas/tile geometry via `MultiviewLayoutState CurrentLayout { get; }` and `event EventHandler LayoutChanged`. + +`MultiviewLayoutState` is a JSON-serializable, product-agnostic model of the canvas: `CanvasWidth`, +`CanvasHeight`, and a list of `MultiviewTileState` entries (`TileNumber`, `TileSinkKey`, `X`, `Y`, +`Width`, `Height`, `ZOrder`, `SourceDeviceKey`) - suitable for driving a routing diagram UI. + +#### Real-time routing feedback over WebSocket + +Essentials Core includes a built-in WebSocket service, `RoutingFeedbackWebsocket` +(`PepperDash.Essentials.Core.Web`), that broadcasts routing state changes to any connected client in +real time - this is what powers the live routing diagram in the +[Essentials Web Config App](https://github.com/PepperDash/essentials-web-config-app) and +[essentials-devtools](https://github.com/PepperDash/essentials-devtools). + +A client starts (or reuses) a session by sending an HTTP `GET` to the `routingFeedbackSession` API +route (e.g. `https://[processor-ip]/cws/[appId]/api/routingFeedbackSession`), which starts the +WebSocket server on a random high port (if not already running), forwards that port on the CS LAN +adapter if present, and returns a JSON body with the connection URL(s): + +```json +{ "url": "wss://[processor-ip]:[port]/routing/join/", "fallbackUrl": "wss://..." } +``` + +Once connected, the client receives a `snapshot` message describing every midpoint route, sink +input, and multiview layout currently active, followed by incremental update messages (each +debounced ~200ms) as things change: + +| Message `type` | Sent when | Payload | +| --- | --- | --- | +| `snapshot` | On connect | `midpointRoutes`, `sinkRoutes`, `layouts` for the whole system | +| `midpointRouteChanged` | An `IRoutingMidpointWithFeedback`'s `RouteChanged` fires | `deviceKey`, `routes: [{ inputPortKey, outputPortKey, signalType }]` | +| `sinkInputChanged` | An `IRoutingSinkWithFeedback`'s `InputChanged` fires | `deviceKey`, `inputPortKey`, `sourceDeviceKey`, `signalType` | +| `layoutChanged` | An `IRoutingSinkWithLayoutState`'s `LayoutChanged` fires | `deviceKey`, `layout` (a `MultiviewLayoutState`) | + +Per-tile sinks belonging to an `IRoutingSinkWithLayouts` device are reported to clients as qualified +input ports on their parent device's node, rather than as separate graph nodes, so a routing diagram +can render a multiview decoder as a single device with per-tile inputs. diff --git a/docs/docs/technical-docs/Mobile-Control.md b/docs/docs/technical-docs/Mobile-Control.md new file mode 100644 index 00000000..0215f897 --- /dev/null +++ b/docs/docs/technical-docs/Mobile-Control.md @@ -0,0 +1,240 @@ +# Mobile Control: architecture and client communication + +## TL;DR + +Mobile Control is a WebSocket-based messaging layer built into Essentials. Devices expose small +"messenger" classes that register JSON message paths (e.g. `/device/{key}/startMeeting`) with a +central `MobileControlSystemController`. Clients (typically a React app built on +[`@pepperdash/mobile-control-react-app-core`](https://github.com/PepperDash/mobile-control-react-app-core)) +connect over WebSocket with a per-room token, send the same JSON envelope to invoke device methods, +and receive unsolicited state-update messages that a Redux store uses to keep device/room state in +sync in real time. + +## Server-side architecture + +### Projects involved + +* `PepperDash.Essentials.MobileControl` - the `MobileControlSystemController` device, its + WebSocket server (`WebSocketServer/MobileControlWebsocketServer.cs`), room bridges, and + touchpanel controller. +* `PepperDash.Essentials.MobileControl.Messengers` (namespace `PepperDash.Essentials.AppServer.Messengers`) - + the `MessengerBase` class and the library of built-in messengers for common device interfaces + (`IHasStartMeetingMessenger`, `IHasMeetingInfoMessenger`, `ICommunicationMonitorMessenger`, + `IRoutingMidpointWithFeedbackMessenger`, etc.), plus the message envelope types. +* `PepperDash.Essentials.Core.DeviceTypeInterfaces` - the public contracts: + `IMobileControl`, `IMobileControlMessenger`, `IMobileControlMessengerWithSubscriptions`, + `IMobileControlRoomMessenger`, `IMobileControlAction`, `IMobileControlMessage`, and the + touchpanel-controller interfaces. + +### Direct Server vs. Edge Server + +Mobile Control supports two (non-exclusive) transport modes, configured on the `MobileControlConfig` +device: + +* **Direct Server** - `MobileControlWebsocketServer` runs an HTTPS WebSocket server directly on the + processor hardware. Clients on the same network connect straight to the processor - lowest + latency, no external dependency. This is what `mobile-control-react-app-core`'s local dev flow + and the deployed `mcUserApp` React app use. +* **Edge Server** (API server / cloud gateway) - the processor instead makes an *outbound* WebSocket + connection to an external Mobile Control server, which relays messages between it and remote + clients. Useful when clients can't reach the processor directly (e.g. no local network access). + +Both modes can be enabled simultaneously; outgoing messages are queued and sent down whichever +transport(s) are active. + +### The WebSocket endpoint (Direct Server) + +`MobileControlWebsocketServer` (`WebSocketServer/MobileControlWebsocketServer.cs`) hosts the +Direct Server: + +* **Path**: `/mc/api/ui/join/` (`_wsPath`) +* **Port**: `50000 + ` by default (e.g. program slot 2 → port `50002`), or a + custom port from config +* **User app**: static files are served from `/user/programX/mcUserApp` (`_appPath`) at the base + href `/mc/app` (`_userAppBaseHref`) - this is exactly the `mcUserApp` deployment directory used + when [deploying](../Get-started.md) a built React app. + +### Connecting and authentication (tokens) + +A client doesn't connect straight to the WebSocket - it first needs a **token** identifying which +room/UI-client slot it's joining: + +1. **Get a token** - on the processor console, run: + ``` + mobileinfo:[programSlot] + ``` + This prints the Direct Server port, any existing UI-client tokens, and full connect URLs (e.g. + `http://[ip]:[port]/mc/app?token=[token]`). New tokens can also be added dynamically + (`mobileadduiclient:[programSlot] [roomKey]`) - see the console command's help text for details. +2. **Join a room** - the client calls `GET {apiPath}/ui/joinroom?token={token}` over HTTPS, which + validates the token and returns room data including a generated `clientId`. +3. **Open the WebSocket** - the client connects to + `wss://[processor-ip]:[port]/mc/api/ui/join/{token}?clientId={clientId}`. + +Each connected client is tracked as a `UiClient`; disconnecting removes its subscriptions from +every messenger it was subscribed to. + +### The message envelope + +Every message sent in either direction is the same simple JSON envelope +(`MobileControlMessage`, namespace `PepperDash.Essentials.AppServer.Messengers`): + +```json +{ + "type": "/device/zoomRoom1/startMeeting", + "clientId": "abc-123", + "content": { "value": 30 } +} +``` + +* `type` - a path identifying the target device/room and action (client → server), or the + device/room whose state is being reported (server → client). +* `clientId` - which client sent the message, or (server → client) which single client a targeted + reply is meant for; omitted/ignored for broadcasts to all subscribers. +* `content` - a `JToken` payload. For simple values, `MobileControlSimpleContent` wraps a single + `value` property (as in the example above); for state updates, `content` is the device's full + serialized state object. + +### The messenger pattern + +Each device that wants to be controllable/observable over Mobile Control gets a small **messenger** +class deriving from `MessengerBase` (`PepperDash.Essentials.AppServer.Messengers`): + +```csharp +public class IHasStartMeetingMessenger : MessengerBase +{ + // ... + protected override void RegisterActions() + { + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + AddAction("/startMeeting", (id, content) => + { + var msg = content.ToObject>(); + _startMeeting.StartMeeting(msg?.Value ?? _startMeeting.DefaultMeetingDurationMin); + }); + + AddAction("/leaveMeeting", (id, content) => _startMeeting.LeaveMeeting()); + } +} +``` + +* The messenger is constructed with a **base `MessagePath`** (e.g. `/device/zoomRoom1`) and calls + `AddAction(subPath, handler)` for each supported action, relative to that base path. +* `RegisterWithAppServer(IMobileControl appServerController)` registers the messenger's + `HandleMessage` callback with the system controller via `IMobileControl.AddAction(...)`. +* When an incoming message's `type` starts with the messenger's `MessagePath`, `HandleMessage` + strips that prefix (leaving e.g. `/startMeeting`) and dispatches to the matching registered + action. The sending client is automatically added to that messenger's subscriber list, so it + receives future unsolicited feedback from it. +* To push state out, messengers call `PostStatusMessage(DeviceStateMessageBase message, clientId)` - + omitting `clientId` broadcasts to every subscribed client; passing one targets a single client + (e.g. replying to a `/fullStatus` request). + +### How devices opt in + +`EssentialsDevice.CustomActivate()` calls a virtual `CreateMobileControlMessengers()` hook once all +devices have activated. A device (or a room) overrides this method to look up the `IMobileControl` +device and construct/register whichever messengers it needs: + +```csharp +protected override void CreateMobileControlMessengers() +{ + var mc = DeviceManager.AllDevices.OfType().FirstOrDefault(); + if (mc == null) return; + + var messenger = new IHasStartMeetingMessenger("zoomRoom1-startMeeting", "/device/zoomRoom1", this); + messenger.RegisterWithAppServer(mc); +} +``` + +Rooms typically implement `IMobileControlRoomMessenger` and register a room-level messenger at a +`/room/{roomKey}` path (via a `MobileControlBridgeBase`/`MobileControlEssentialsRoomBridge`), in +addition to any per-device messengers. + +## Client-side architecture (React) + +`@pepperdash/mobile-control-react-app-core` provides the client building blocks used by apps like +[beincourt-pv2-react-app](https://github.com/pepperdash-beincourt/beincourt-pv2-react-app): + +### Connection lifecycle + +The real work happens in a Redux middleware (`src/lib/store/middleware/websocketMiddleware.ts`); +`WebsocketProvider`/`WebsocketContext` are a thin, backward-compatible React Context wrapper around +it that simply dispatches Redux actions (`wsConnect`, `wsSendMessage`, `wsReconnect`, ...). + +1. On mount, `wsConnect()` is dispatched. +2. The middleware reads `apiPath` from the app's local config (`_config.local.json`/ + `_config.default.json`) and the connection `token` (from the URL's `?token=` query param). +3. It calls `GET {apiPath}/ui/joinroom?token={token}` to validate the token and get room data + (including a `clientId`). +4. It opens a WebSocket at `{apiPath with ws(s) scheme}/ui/join/{token}?clientId={clientId}`. +5. On specific close codes (e.g. `4000` user code changed, `4002` room combination changed) it stops + auto-reconnecting and surfaces an error; otherwise it automatically retries. + +### Sending messages + +```ts +const { sendMessage } = useWebsocketContext(); +sendMessage('/device/zoomRoom1/startMeeting', { value: 30 }); +``` + +`sendMessage` serializes `{ type, clientId, content }` (the same envelope described above) and +sends it over the open WebSocket. + +### Receiving messages / state hydration + +Incoming messages are routed by their `type` prefix: + +| Prefix | Handling | +| --- | --- | +| `/system/*` | Internal system messages (user code, touchpanel key, room-combination/device-interface changes, initial sync complete, ...) | +| `/event/*` | Dispatched to any handlers registered via `addEventHandler(eventType, key, callback)` | +| `/room/*` | `dispatch(roomsActions.setRoomState(message))` | +| `/device/*` | `dispatch(devicesActions.setDeviceState(message))` | + +Device/room state lives in Redux slices keyed by device/room key, so any component can read the +latest known state for a given key. + +### The hook pattern + +Each supported device interface has a small hook that combines reading state with calling actions, +e.g. `useIHasStartMeeting`: + +```ts +export function useIHasStartMeeting(key: string): IHasStartMeetingReturn | undefined { + const { sendMessage } = useWebsocketContext(); + const state = useGetDevice(key); // reads devices[key] from Redux + + return useMemo(() => { + if (!state) return undefined; + const path = `/device/${key}`; + return { + state, + startMeeting: (durationMin?: number) => sendMessage(`${path}/startMeeting`, { value: durationMin }), + leaveMeeting: () => sendMessage(`${path}/leaveMeeting`, null), + }; + }, [key, sendMessage, state]); +} +``` + +Components use these hooks to read live device state and call device actions without needing to +know anything about the WebSocket transport underneath. + +## Full round-trip example: starting a meeting + +1. **UI**: user clicks "Start Meeting"; component calls `startMeeting(30)` from `useIHasStartMeeting('zoomRoom1')`. +2. **Client → server**: `sendMessage('/device/zoomRoom1/startMeeting', { value: 30 })` sends + `{"type":"/device/zoomRoom1/startMeeting","clientId":"abc-123","content":{"value":30}}` over the WebSocket. +3. **Server routing**: the system controller matches `/device/zoomRoom1` against the registered + `IHasStartMeetingMessenger`'s `MessagePath`, strips the prefix to get `/startMeeting`, and invokes + its registered action, which calls `StartMeeting(30)` on the underlying device. +4. **Device fires feedback**: the device's meeting-started feedback fires. +5. **Server → clients**: the messenger calls `PostStatusMessage(...)`, broadcasting + `{"type":"/device/zoomRoom1","clientId":null,"content":{ ...state }}` to every client subscribed + to that messenger. +6. **Client receives**: the message's `type` starts with `/device/`, so + `dispatch(devicesActions.setDeviceState(message))` merges the new state into Redux under + `devices['zoomRoom1']`. +7. **UI updates**: `useIHasStartMeeting('zoomRoom1')`'s `state` reflects the new meeting info on the + next render. diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml index 820de992..9ec7ee87 100644 --- a/docs/docs/toc.yml +++ b/docs/docs/toc.yml @@ -38,6 +38,8 @@ href: technical-docs/Plugins.md - name: Communication Basics href: technical-docs/Communication-Basics.md + - name: Mobile Control + href: technical-docs/Mobile-Control.md - name: Debugging href: technical-docs/Debugging.md - name: Feedback Classes From 77e6ba7f36b2d330ea8098301518445765122a0c Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 10 Aug 2026 22:00:22 -0600 Subject: [PATCH 143/161] feat: make IsInCall property virtual in VideoCodecBase class --- .../VideoCodec/VideoCodecBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index fceee089..0d776703 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -258,7 +258,7 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingSource, /// /// Returns true when any call is not in state Unknown, Disconnecting, Disconnected /// - public bool IsInCall + public virtual bool IsInCall { get { From 7fca4d74bf786384d3a65e63a443b759cb174e12 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 11 Aug 2026 14:16:56 -0600 Subject: [PATCH 144/161] feat: add IAnnotationStream interface for annotation stream functionality --- .../DeviceTypeInterfaces/IAnnotationStream.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAnnotationStream.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAnnotationStream.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAnnotationStream.cs new file mode 100644 index 00000000..7bef3671 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IAnnotationStream.cs @@ -0,0 +1,19 @@ +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + + /// + /// Defines minimum functionality for a device that can provide an annotation stream. + /// + public interface IAnnotationStream + { + /// + /// Starts the annotation stream. + /// + /// The index of the annotation stream to start. + bool StartAnnotationForStream(ushort streamIdx); + + /// + /// Stops the annotation stream. + /// + /// The index of the annotation stream to stop. + bool StopAnnotationForStream(ushort streamIdx); + } From 1a5db87c1ecacfec0a458907728d3ab307c4dfcb Mon Sep 17 00:00:00 2001 From: equinoy Date: Wed, 12 Aug 2026 09:16:29 -0500 Subject: [PATCH 145/161] fix: null-safe CodecInfo access in AudioCodecBaseMessenger Codec.CodecInfo is null during DSP startup/reconnect. Guard with null- conditional to prevent NullReferenceException from propagating up through the CallStatusChange event chain. --- .../Messengers/AudioCodecBaseMessenger.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs new file mode 100644 index 00000000..7eb3d5aa --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs @@ -0,0 +1,120 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Devices.Common.AudioCodec; +using PepperDash.Essentials.Devices.Common.Codec; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for an AudioCodecBase device + /// + public class AudioCodecBaseMessenger : MessengerBase + { + /// + /// Device being bridged + /// + public AudioCodecBase Codec { get; private set; } + + /// + /// Constuctor + /// + /// + /// + /// + public AudioCodecBaseMessenger(string key, AudioCodecBase codec, string messagePath) + : base(key, messagePath, codec) + { + Codec = codec ?? throw new ArgumentNullException("codec"); + codec.CallStatusChange += Codec_CallStatusChange; + } + + /// + protected override void RegisterActions() + + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendAtcFullMessageObject(id)); + + AddAction("/audioDialerStatus", (id, content) => SendAtcFullMessageObject(id)); + + AddAction("/dial", (id, content) => + { + var msg = content.ToObject>(); + + Codec.Dial(msg.Value); + }); + + AddAction("/endCallById", (id, content) => + { + var msg = content.ToObject>(); + + var call = GetCallWithId(msg.Value); + if (call != null) + Codec.EndCall(call); + }); + + AddAction("/endAllCalls", (id, content) => Codec.EndAllCalls()); + AddAction("/dtmf", (id, content) => + { + var msg = content.ToObject>(); + + Codec.SendDtmf(msg.Value); + }); + + AddAction("/rejectById", (id, content) => + { + var msg = content.ToObject>(); + + var call = GetCallWithId(msg.Value); + + if (call != null) + Codec.RejectCall(call); + }); + + AddAction("/acceptById", (id, content) => + { + var msg = content.ToObject>(); + var call = GetCallWithId(msg.Value); + if (call != null) + Codec.AcceptCall(call); + }); + } + + /// + /// Helper to grab a call with string ID + /// + /// + /// + private CodecActiveCallItem GetCallWithId(string id) + { + return Codec.ActiveCalls.FirstOrDefault(c => c.Id == id); + } + + private void Codec_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e) + { + SendAtcFullMessageObject(); + } + + /// + /// Helper method to build call status for vtc + /// + /// + private void SendAtcFullMessageObject(string id = null) + { + var info = Codec.CodecInfo; + + PostStatusMessage(JToken.FromObject(new + { + isInCall = Codec.IsInCall, + calls = Codec.ActiveCalls, + info = new + { + phoneNumber = info?.PhoneNumber + } + }), id + ); + } + } +} \ No newline at end of file From 2588b81f0d1de04d8eee7ba9e6a6e6a9dbf13a72 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 12 Aug 2026 10:59:01 -0600 Subject: [PATCH 146/161] feat: add error handling and logging to SendAtcFullMessageObject method in IDialerCallStatusMessenger --- .../Messengers/IDialerCallStatusMessenger.cs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs index a1c6e5d7..4cee7d17 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDialerCallStatusMessenger.cs @@ -2,6 +2,7 @@ using System.Linq; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Codec; @@ -103,18 +104,25 @@ namespace PepperDash.Essentials.AppServer.Messengers /// private void SendAtcFullMessageObject(string id = null) { - var info = Codec.CodecInfo; - - PostStatusMessage(JToken.FromObject(new + try { - isInCall = Codec.IsInCall, - calls = Codec.ActiveCalls, - info = new + var info = Codec.CodecInfo; + + PostStatusMessage(JToken.FromObject(new { - phoneNumber = info.PhoneNumber - } - }), id - ); + isInCall = Codec.IsInCall, + calls = Codec.ActiveCalls, + info = new + { + phoneNumber = info?.PhoneNumber + } + }), id + ); + } + catch (Exception ex) + { + this.LogError(ex, "Error sending dialer call status full message"); + } } } } \ No newline at end of file From da2a09f7cbc19e95a4ba54a65b40e8bfec24dad3 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 13 Aug 2026 12:06:13 -0600 Subject: [PATCH 147/161] feat: add INetworkSwitchVlanManager and INetworkSwitchPoeManager interfaces for VLAN and PoE control --- .../INetworkSwitchControl.cs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs new file mode 100644 index 00000000..ba3215fe --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -0,0 +1,122 @@ +using System; + +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Interface for network switches that support VLAN assignment on individual ports. +/// +public interface INetworkSwitchVlanManager +{ + /// + /// Returns the current access VLAN ID configured on the port. + /// Return -1 when the value is unavailable (e.g. the switch has not been polled yet + /// or the implementation does not support VLAN queries). + /// + /// Switch port identifier + /// VLAN ID or -1 when unavailable + int GetPortCurrentVlan(string port); + + /// + /// Changes the access VLAN of a single switch port. + /// The implementation is responsible for entering/exiting privileged/config mode. + /// + /// Switch port identifier (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco) + /// Target VLAN ID (1-4093) + void SetPortVlan(string port, uint vlanId); +} + +/// +/// Interface for network switches that support Power over Ethernet (PoE) control on individual ports. +/// +public interface INetworkSwitchPoeManager +{ + /// + /// Enables or disables PoE power delivery on a single switch port. + /// The implementation is responsible for entering/exiting privileged/config mode. + /// + /// Switch port identifier + /// True to enable PoE; false to disable PoE + void SetPortPoeState(string port, bool enabled); +} + +/// +/// Standardized interface for network switch devices that support per-port PoE control +/// and VLAN assignment. +/// +public interface INetworkSwitchPoeVlanManager : INetworkSwitchVlanManager, INetworkSwitchPoeManager +{ + /// + /// Event that is raised when the state of a switch port changes, such as a VLAN change or PoE state change. + /// + event EventHandler PortStateChanged; + +} + +/// +/// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes. +/// +public class NetworkSwitchPortEventArgs : EventArgs +{ + /// + /// The identifier of the port that changed state (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco). + /// + public string Port { get; private set; } + + /// + /// The type of event that occurred on the port (e.g. VLAN change, PoE enabled/disabled). + /// + public NetworkSwitchPortEventType EventType { get; private set; } + + /// + /// Constructor for NetworkSwitchPortEventArgs + /// + /// The identifier of the port that changed state + /// The type of event that occurred on the port + public NetworkSwitchPortEventArgs(string port, NetworkSwitchPortEventType eventType) + { + Port = port; + EventType = eventType; + } +} + +/// +/// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes. +/// +public enum NetworkSwitchPortEventType +{ + /// + /// Indicates that the type of event is unknown or cannot be determined. + /// + Unknown, + + /// + /// Indicates that a VLAN change is in progress on the port, either through a call to SetPortVlan or an external change detected by polling. + /// + VlanChangeInProgress, + + /// + /// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan + /// + VlanChanged, + + /// + /// Indicates that PoE is being disabled on the port, either through a call to SetPortPoeState or an external change detected by polling. + /// + PoeDisableInProgress, + + /// + /// Indicates that the PoE state on a port has changed, either through a successful call to SetPortPoeState + /// + PoEDisabled, + + /// + /// Indicates that PoE is being enabled on the port, either through a call to SetPortPoeState or an external change detected by polling. + /// + PoeEnableInProgress, + + /// + /// Indicates that the PoE state on a port has changed, either through a successful call to SetPortPoeState + /// + PoEEnabled +} + From 0d4fd7773e65d22179a1978ed753d2b6c866a4e2 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 13 Aug 2026 16:03:51 -0600 Subject: [PATCH 148/161] feat: update documentation links and remove obsolete files --- README.md | 2 +- docs/docs/Home.md | 3 +- .../CrestronGenericBaseDevice.cs.orig | 128 ------------------ .../PepperDash.Essentials.Core.csproj | 6 - ...perDash_Essentials_Core.csproj.DotSettings | 3 - .../SomeWebSocketClass.cs | 37 ----- 6 files changed, 3 insertions(+), 176 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs.orig delete mode 100644 src/PepperDash.Essentials.Core/PepperDash_Essentials_Core.csproj.DotSettings delete mode 100644 src/PepperDash.Essentials.Core/SomeWebSocketClass.cs diff --git a/README.md b/README.md index b485eade..fac87250 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Utilization of Essentials Framework falls into the following categories: ## Documentation -For detailed documentation, see the [Wiki](https://github.com/PepperDash/EssentialsFramework/wiki). +For detailed documentation, see the [DocFx Site](https://pepperdash.github.io/Essentials/). ## Support diff --git a/docs/docs/Home.md b/docs/docs/Home.md index 8e0c163e..bdda2185 100644 --- a/docs/docs/Home.md +++ b/docs/docs/Home.md @@ -19,12 +19,13 @@ Or use the links to the right to navigate our documentation. ## Benefits -- Runs on Crestron 3-Series, **4-Series** and VC-4 Control System platforms +- Runs on Crestron **4-Series** and VC-4 Control System platforms - Reduced hardware overhead compared to S+ and Simpl solutions - Quick development cycle - Shared resources made easily available - More flexibility with less code - Configurable using simple JSON files +- Built in websocket communication API for HTML5 frontend applications. - Is awesome --- diff --git a/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs.orig b/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs.orig deleted file mode 100644 index 8dc38428..00000000 --- a/src/PepperDash.Essentials.Core/Crestron/CrestronGenericBaseDevice.cs.orig +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using Crestron.SimplSharpPro; -using Crestron.SimplSharpPro.DeviceSupport; - -using PepperDash.Core; - -namespace PepperDash.Essentials.Core -{ - /// - /// A bridge class to cover the basic features of GenericBase hardware - /// - public class CrestronGenericBaseDevice : Device, IOnline, IHasFeedback, ICommunicationMonitor, IUsageTracking - { - public virtual GenericBase Hardware { get; protected set; } - - public BoolFeedback IsOnline { get; private set; } - public BoolFeedback IsRegistered { get; private set; } - public StringFeedback IpConnectionsText { get; private set; } - - public CrestronGenericBaseDevice(string key, string name, GenericBase hardware) - : base(key, name) - { - Hardware = hardware; - IsOnline = new BoolFeedback(CommonBoolCue.IsOnlineFeedback, () => Hardware.IsOnline); - IsRegistered = new BoolFeedback(new Cue("IsRegistered", 0, eCueType.Bool), () => Hardware.Registered); - IpConnectionsText = new StringFeedback(CommonStringCue.IpConnectionsText, () => - string.Join(",", Hardware.ConnectedIpList.Select(cip => cip.DeviceIpAddress).ToArray())); - CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, hardware, 120000, 300000); - } - - /// - /// Make sure that overriding classes call this! - /// Registers the Crestron device, connects up to the base events, starts communication monitor - /// - protected override bool CustomActivate() - { - Debug.LogMessage(LogEventLevel.Information, this, "Activating"); - var response = Hardware.RegisterWithLogging(Key); - if (response != eDeviceRegistrationUnRegistrationResponse.Success) - { -<<<<<<< HEAD - Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Crestron device: {0}", response); - return false; - } -======= - Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Crestron device: {0}", response); - return false; - } ->>>>>>> origin/feature/ecs-342-neil - Hardware.OnlineStatusChange += new OnlineStatusChangeEventHandler(Hardware_OnlineStatusChange); - CommunicationMonitor.Start(); - - return true; - } - - /// - /// This disconnects events and unregisters the base hardware device. - /// - /// - public override bool Deactivate() - { - CommunicationMonitor.Stop(); - Hardware.OnlineStatusChange -= Hardware_OnlineStatusChange; - - return Hardware.UnRegister() == eDeviceRegistrationUnRegistrationResponse.Success; - } - - /// - /// Returns a list containing the Outputs that we want to expose. - /// - public virtual List Feedbacks - { - get - { - return new List - { - IsOnline, - IsRegistered, - IpConnectionsText - }; - } - } - - void Hardware_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) - { - IsOnline.FireUpdate(); - } - - #region IStatusMonitor Members - - public StatusMonitorBase CommunicationMonitor { get; private set; } - #endregion - - #region IUsageTracking Members - - public UsageTracking UsageTracker { get; set; } - - #endregion - } - - //*********************************************************************************** - public class CrestronGenericBaseDeviceEventIds - { - public const uint IsOnline = 1; - public const uint IpConnectionsText =2; - } - - /// - /// Adds logging to Register() failure - /// - public static class GenericBaseExtensions - { - public static eDeviceRegistrationUnRegistrationResponse RegisterWithLogging(this GenericBase device, string key) - { - var result = device.Register(); - if (result != eDeviceRegistrationUnRegistrationResponse.Success) - { - Debug.LogMessage(LogEventLevel.Information, "Cannot register device '{0}': {1}", key, result); - } - return result; - } - - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index 3aa65e84..983155a5 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -28,12 +28,6 @@ - - - - - - diff --git a/src/PepperDash.Essentials.Core/PepperDash_Essentials_Core.csproj.DotSettings b/src/PepperDash.Essentials.Core/PepperDash_Essentials_Core.csproj.DotSettings deleted file mode 100644 index cb991a69..00000000 --- a/src/PepperDash.Essentials.Core/PepperDash_Essentials_Core.csproj.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - True - False \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/SomeWebSocketClass.cs b/src/PepperDash.Essentials.Core/SomeWebSocketClass.cs deleted file mode 100644 index b87e87ae..00000000 --- a/src/PepperDash.Essentials.Core/SomeWebSocketClass.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; - -public class SomeWebSocketClass -{ - private ClientWebSocket _webSocket; - - public SomeWebSocketClass() - { - _webSocket = new ClientWebSocket(); - } - - public async Task ConnectAsync(Uri uri) - { - await _webSocket.ConnectAsync(uri, CancellationToken.None); - } - - public async Task SendAsync(string message) - { - var buffer = System.Text.Encoding.UTF8.GetBytes(message); - await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); - } - - public async Task ReceiveAsync() - { - var buffer = new byte[1024]; - var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); - } - - public async Task CloseAsync() - { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } -} From e9812f7858ad9e1711fa7043f9fa447b3e958518 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 14 Aug 2026 13:16:49 -0600 Subject: [PATCH 149/161] refactor: update documentation comments in IHasCresnetBranches interface --- .../CrestronIO/IHasCresnetBranches.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/PepperDash.Essentials.Core/CrestronIO/IHasCresnetBranches.cs b/src/PepperDash.Essentials.Core/CrestronIO/IHasCresnetBranches.cs index b882f50e..449d59f3 100644 --- a/src/PepperDash.Essentials.Core/CrestronIO/IHasCresnetBranches.cs +++ b/src/PepperDash.Essentials.Core/CrestronIO/IHasCresnetBranches.cs @@ -9,18 +9,12 @@ using Crestron.SimplSharpPro.DeviceSupport; namespace PepperDash.Essentials.Core; /// -/// Defines the contract for a class that has Cresnet branches +/// Defines the contract for IHasCresnetBranches /// public interface IHasCresnetBranches { /// - /// Defines the contract for IHasCresnetBranches + /// Collection of Cresnet branches /// - public interface IHasCresnetBranches - { - /// - /// Collection of Cresnet branches - /// - CrestronCollection CresnetBranches { get; } - } -} \ No newline at end of file + CrestronCollection CresnetBranches { get; } +} From 51926c07dd140c763ea7b77d8c4aa731c8d62184 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 14 Aug 2026 13:19:17 -0600 Subject: [PATCH 150/161] (force-patch): generate build From 72844aa6ca21e321fd3e284c2a9e975e117946d8 Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Wed, 19 Aug 2026 13:51:11 -0400 Subject: [PATCH 151/161] feat: add IHasWirelessSharing interface for wireless presentation devices --- .../IHasWirelessSharing.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs new file mode 100644 index 00000000..bf7562fc --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs @@ -0,0 +1,47 @@ +using System; + +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Defines the contract for a wireless presentation endpoint that reports whether a wireless +/// sharing session is currently active. Implemented by platforms such as Crestron AirMedia, +/// Mersive Solstice, Barco ClickShare, Miracast/Teams receivers, etc. Allows consumers (e.g. +/// room plugins) to react to wireless sharing activity without taking a dependency on any +/// concrete device implementation. +/// +/// +/// This refers specifically to wireless screen/device mirroring, as distinct from in-call +/// content sharing on a video conference. +/// +public interface IHasWirelessSharing +{ + /// + /// Reports whether a wireless sharing session is currently active (content is being presented). + /// + BoolFeedback IsSharingFeedback { get; } + + /// + /// Raised when wireless sharing starts or stops. The event args carry the new sharing state. + /// + event EventHandler SharingChanged; +} + +/// +/// Event arguments describing a change in wireless sharing state. +/// +public class WirelessSharingEventArgs : EventArgs +{ + /// + /// True if a wireless sharing session is active (content is being presented), false otherwise. + /// + public bool IsSharing { get; private set; } + + /// + /// Creates a new . + /// + /// True if a wireless sharing session is active, false otherwise. + public WirelessSharingEventArgs(bool isSharing) + { + IsSharing = isSharing; + } +} From 8c908c80e27647025e4f73b3ea29e18e0e7d0d73 Mon Sep 17 00:00:00 2001 From: equinoy Date: Thu, 20 Aug 2026 14:31:29 -0500 Subject: [PATCH 152/161] feat: add IHasMultiviewControl interface for multiview enable and layout feedback Exposes SetMultiviewEnabled plus MultiviewEnabled/MultiviewLayout feedbacks on an Essentials Core interface so room plugins can drive a multiview/UVC compositor (e.g. StreamSync) without a compile-time dependency on the hardware plugin. Layout selection remains on IHasScreensWithLayouts.ApplyLayout. --- .../IHasMultiviewControl.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMultiviewControl.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMultiviewControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMultiviewControl.cs new file mode 100644 index 00000000..58bdd1f3 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasMultiviewControl.cs @@ -0,0 +1,28 @@ +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; + +/// +/// Defines a device (e.g. a multiview/UVC compositor) whose multiview output can be enabled and +/// whose active layout can be observed. Implementing this interface (instead of requiring consumers +/// to reference the hardware-specific plugin type directly) lets other plugins (e.g. a room plugin) +/// turn the multiview on and read its current layout without taking a compile-time dependency on the +/// specific hardware plugin that implements it. The layout is set via +/// . +/// +public interface IHasMultiviewControl +{ + /// + /// Enables or disables the multiview output. + /// + /// True to enable multiview, false to disable. + void SetMultiviewEnabled(bool enabled); + + /// + /// Feedback for whether the multiview output is currently enabled. + /// + BoolFeedback MultiviewEnabled { get; } + + /// + /// Feedback for the current multiview layout index. + /// + IntFeedback MultiviewLayout { get; } +} From 7e466fbed162c0c6c344ea24899eec21abc563b0 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 21 Aug 2026 09:52:21 -0600 Subject: [PATCH 153/161] chore(force-patch): generate new build From fd0a612c1a31606e6f62bba282e52a7aed8afcd0 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 21 Aug 2026 12:22:03 -0600 Subject: [PATCH 154/161] chore(force-patch): generate new build From 33e56d32593e60b41c918571e88cd2c34c25431f Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 21 Aug 2026 12:23:23 -0600 Subject: [PATCH 155/161] feat: enhance IHasWebView interface with detailed webview event handling and error reporting --- .../DeviceTypeInterfaces/IHasWebView.cs | 452 +++++++++++++++--- 1 file changed, 377 insertions(+), 75 deletions(-) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWebView.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWebView.cs index d1ae55bd..ef766efd 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWebView.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWebView.cs @@ -1,84 +1,386 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; -namespace PepperDash.Essentials.Core.DeviceTypeInterfaces; - - -/// -/// Defines the contract for IHasWebView -/// -public interface IHasWebView +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces { - /// - /// Indicates whether the webview is currently visible - /// - bool WebviewIsVisible { get; } + /// + /// Defines the display mode for a webview event, with expected values of "Fullscreen", "Modal", or "Unknown". + /// + public enum eWebViewEventMode + { + /// + /// The display mode for the webview event is unknown or not specified. This value can be used as a default or fallback when the display mode is not provided or cannot be parsed into a known value. + /// + Unknown, + + /// + /// The webview event should be displayed in fullscreen mode, covering the entire screen and typically used for immersive experiences or when maximum screen real estate is needed. When a webview event with this display mode is shown, it will typically trigger the WebViewStatusChanged event with a status of "Fullscreen", and when it is cleared/closed, it will trigger the WebViewStatusChanged event with a status of "Cleared". + /// + Fullscreen, + + /// + /// The webview event should be displayed in modal mode, which typically means it will be shown as a dialog or overlay on top of the existing content, allowing the user to interact with it while still being able to see the underlying content. This display mode is often used for alerts, confirmations, or when the webview content is related to the current context but does not require full immersion. When a webview event with this display mode is shown, it will typically trigger the WebViewStatusChanged event with a status of "Modal", and when it is cleared/closed, it will trigger the WebViewStatusChanged event with a status of "Cleared". + /// + Modal, + } + + /// + /// Defines the target for a webview event, with expected values of "OSD", "Controller", "PersistentWebApp", or "RoomScheduler". + /// + + public enum eWebViewTarget + { + /// + /// The target for the webview event is unknown or not specified. This value can be used as a default or fallback when the target is not provided or cannot be parsed into a known value. + /// + Unknown, + + /// + /// The webview event should be displayed on the On-Screen Display (OSD). + /// + OSD, + + /// + /// The webview event should be displayed on the controller. + /// + Controller, + + /// + /// The webview event should be displayed on the persistent web application. + /// + PersistentWebApp, + + /// + /// The webview event should be displayed on the room scheduler. + /// + RoomScheduler + } + + /// + /// Represents the reason for an error in a webview event, which can provide additional information about what went wrong. This class is typically only used in the Status property of a WebViewEvent when the status indicates an error, and may be null otherwise. + /// + public class Reason + { + /// + /// The reason for an error in a webview event as a string, which can provide additional information about what went wrong. This property is typically only populated in case of an error, and may be null otherwise. + /// + public string Value { get; set; } + } + + /// + /// Represents the XPath of a webview event, which can provide information about where an error occurred in the webview. This class is typically only used in the Status property of a WebViewEvent when the status indicates an error, and may be null otherwise. + /// + public class XPath + { + /// + /// The XPath of a webview event as a string, which can provide information about where an error occurred in the webview. This property is typically only populated in case of an error, and may be null otherwise. + /// + public string Value { get; set; } + } + + /// + /// Represents a base class for properties that have a string value and trigger an action when the value changes. This class can be used as a base for properties like DisplayMode and Target in the WebViewEvent, which have string values that can be set directly or parsed into enums for easier handling of expected values. The ValueChangedAction can be set to trigger any desired behavior when the value changes, such as updating the UI or triggering other events. + /// + public abstract class ValueProperty + { + /// + /// Triggered when Value is set + /// + public Action ValueChangedAction { get; set; } + + /// + /// Triggers the ValueChangedAction if it is set. This method should be called whenever the Value property is set to ensure that any desired behavior associated with a change in value is executed. + /// + protected void OnValueChanged() + { + var a = ValueChangedAction; + if (a != null) + a(); + } + + } + + /// + /// Represents a webview event, which can include information about the status of the webview, the display parameters for the webview, and any error information if applicable. This class can be used to represent both show and clear events for a webview, with the Status property indicating the current status of the webview (e.g., "Fullscreen", "Modal", "Cleared", "Error", or "Unknown"), the Display property providing details about how the webview is being displayed (e.g., mode, URL, target, title), and the Cleared property providing details about a cleared/closed webview event (e.g., target and ID). The Id property can be used to correlate show and clear events for the same webview instance. + /// + public class WebViewEvent + { + /// + /// The unique identifier for the webview event, which can be used to correlate show and clear events for the same webview instance. This property is typically included in both show and clear events for a webview, allowing you to track the lifecycle of a specific webview instance from when it is shown to when it is cleared/closed. The Id can be any string value, but it should be unique for each webview instance to ensure proper correlation between show and clear events. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The status of the webview event, which can indicate the current state of the webview (e.g., "Fullscreen", "Modal", "Cleared", "Error", or "Unknown") as well as any error information if applicable (XPath and Reason). The Value property can be used to get or set the current status of the webview, while the XPath and Reason properties can provide additional information in case of an error. The StatusString property can be used to get or set the raw status string from the event, but it is recommended to use the Value property for easier handling of expected values. Setting the Value property will trigger the ValueChangedAction if it is set, allowing you to respond to changes in the webview status as needed. + /// + [JsonProperty("status")] + public Status Status { get; set; } // /Event/UserInterface/WebView/Status + + /// + /// The display parameters for the webview event, which can include the display mode (e.g., "Fullscreen", "Modal", or "Unknown"), the URL to display in the webview, the target for the webview (e.g., "OSD", "Controller", "PersistentWebApp", or "RoomScheduler"), and the title to display on the webview. This property is typically included in show events for a webview, providing details about how the webview is being displayed. When a webview event with these display parameters is shown, it will typically trigger the WebViewStatusChanged event with a status of "Fullscreen" or "Modal" depending on the specified display mode, and when it is cleared/closed, it will trigger the WebViewStatusChanged event with a status of "Cleared". + /// + [JsonProperty("display")] + public WebViewDisplay Display { get; set; } // /Event/UserInterface/WebView/Display + + /// + /// The details for a cleared/closed webview event, which can include the target for the webview that was cleared (e.g., "OSD", "Controller", "PersistentWebApp", or "RoomScheduler") and the unique identifier for the webview event that was cleared. This property is typically included in clear events for a webview, providing details about which webview instance was cleared/closed. When a webview event with this property is cleared/closed, it will typically trigger the WebViewStatusChanged event with a status of "Cleared". + /// + [JsonProperty("cleared")] + public WebViewClear Cleared { get; set; } // /Event/UserInterface/WebView/Cleared + } + + /// + /// Represents the display parameters for a webview event, which can include the display mode (e.g., "Fullscreen", "Modal", or "Unknown"), the URL to display in the webview, the target for the webview (e.g., "OSD", "Controller", "PersistentWebApp", or "RoomScheduler"), and the title to display on the webview. This class is typically used in the Display property of a WebViewEvent to provide details about how the webview is being displayed when a show event occurs. When a webview event with these display parameters is shown, it will typically trigger the WebViewStatusChanged event with a status of "Fullscreen" or "Modal" depending on the specified display mode, and when it is cleared/closed, it will trigger the WebViewStatusChanged event with a status of "Cleared". + /// + public class WebViewDisplay + { + /// + /// The display mode for the webview event. Expected values are "Fullscreen", "Modal", or "Unknown". + /// + [JsonProperty("mode")] + public DisplayMode Mode { get; set; } + + /// + /// The URL to display in the webview. + /// + [JsonProperty("url")] + public string Url { get; set; } + + /// + /// The target for the webview. Expected values are "OSD", "Controller", "PersistentWebApp", or "RoomScheduler". + /// + [JsonProperty("target")] + public Target Target { get; set; } + + /// + /// The title to display on the webview. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// The unique identifier for the webview event, used to correlate show and clear events for the same webview instance. + /// + [JsonProperty("id")] + public string Id { get; set; } + } + + /// + /// Represents the data for a webview cleared event, which indicates that a webview with the specified ID and target has been cleared/closed. + /// + public class WebViewClear + { + /// + /// The target for the webview that was cleared. Expected values are "OSD", "Controller", "PersistentWebApp", or "RoomScheduler". + /// + [JsonProperty("target")] + public Target Target { get; set; } + + /// + /// The unique identifier for the webview event that was cleared, used to correlate show and clear events for the same webview instance. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The URL that was displayed in the webview that was cleared. + /// + [JsonProperty("url")] + public string Url { get; set; } + } + + /// + /// Represents the display mode for a webview event, with a string value and a corresponding enum property for easier handling of expected values. + /// + public class DisplayMode : ValueProperty + { + private string _value; + + /// + /// The id of the webview event. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The string value for the display mode, which can be set directly or parsed into the WebViewEventMode enum using the WebViewEventMode property. Setting this property will also trigger the ValueChangedAction if it is set. + /// + public string Value { get { return _value; } set { _value = value; OnValueChanged(); } } + + /// + /// The display mode for the webview event as an enum, which can be used for easier handling of expected values. Expected values are Fullscreen, Modal, or Unknown. + /// + public eWebViewEventMode WebViewEventMode + { + get + { + eWebViewEventMode mode; + System.Enum.TryParse(Value, true, out mode); + return mode; + } + } + } + + /// + /// Represents the target for a webview event, with a string value and a corresponding enum property for easier handling of expected values. Setting the Value property will also trigger the ValueChangedAction if it is set. + /// + public class Target : ValueProperty + { + private string _value; + + /// + /// The id of the webview event. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The string value for the target, which can be set directly or parsed into the eWebViewTarget enum using the WebViewTarget property. Expected values are "OSD", "Controller", "PersistentWebApp", or "RoomScheduler". Setting this property will also trigger the ValueChangedAction if it is set. + /// + public string Value { get { return _value; } set { _value = value; OnValueChanged(); } } + + /// + /// The target for the webview event as an enum, which can be used for easier handling of expected values. Expected values are OSD, Controller, PersistentWebApp, or RoomScheduler. + /// + public eWebViewTarget WebViewTarget + { + get + { + eWebViewTarget target; + System.Enum.TryParse(Value, true, out target); + return target; + } + } + } + + /// + /// Represents the status of a webview event, which can include error information (XPath and Reason) as well as the current status of the webview. The Value property can be used to get or set the current status of the webview, while the XPath and Reason properties can provide additional information in case of an error. The StatusString property can be used to get or set the raw status string from the event. + /// + public class Status + { + /// + /// The XPath of the webview event, which can provide information about where an error occurred in the webview. This property is typically only populated in case of an error, and may be null otherwise. + /// + [JsonProperty("XPath", NullValueHandling = NullValueHandling.Ignore)] + public XPath XPath { get; set; } + + /// + /// The reason for an error in the webview event, which can provide additional information about what went wrong. This property is typically only populated in case of an error, and may be null otherwise. + /// + [JsonProperty("Reason", NullValueHandling = NullValueHandling.Ignore)] + public Reason Reason { get; set; } + + /// + /// The raw status string from the webview event, which can provide information about the current status of the webview. This property can be used to get or set the status directly, but it is recommended to use the Value property for easier handling of expected values. Setting this property will not trigger any actions, while setting the Value property will trigger the ValueChangedAction if it is set. + /// + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public string StatusString { get; set; } + + /// + /// The current status of the webview as a string, which can be set directly or parsed into a WebViewEventMode enum using the WebViewEventMode property. Expected values are "Fullscreen", "Modal", "Cleared", "Error", or "Unknown". Setting this property will trigger the ValueChangedAction if it is set. + /// + [JsonProperty("Value", NullValueHandling = NullValueHandling.Ignore)] + public string Value { get; set; } + } /// - /// Shows the webview with the specified parameters + /// Defines the contract for IHasWebView /// - /// the URL to display in the webview - /// the display mode for the webview - /// the title to display on the webview - /// the target for the webview - void ShowWebView(string url, string mode, string title, string target); - - /// - /// Hides the webview - /// - void HideWebView(); - - /// - /// Event raised when the webview status changes - /// - event EventHandler WebViewStatusChanged; -} - - -/// -/// Defines the contract for IHasWebViewWithPwaMode -/// -public interface IHasWebViewWithPwaMode : IHasWebView -{ - /// - /// Indicates whether the webview is currently in PWA mode - /// - bool IsInPwaMode { get; } - - /// - /// Gets the BoolFeedback indicating whether the webview is currently in PWA mode - /// - BoolFeedback IsInPwaModeFeedback { get; } - - /// - /// Sends navigators to the specified PWA URL. Accepts an absolute URL or a relative URL for a mobile control app - /// - /// The URL to navigate to - void SendNavigatorsToPwaUrl(string url); - - /// - /// Exits navigators from PWA mode - /// - void ExitNavigatorsPwaMode(); -} - - -/// -/// Represents a WebViewStatusChangedEventArgs -/// -public class WebViewStatusChangedEventArgs : EventArgs -{ - /// - /// Gets or sets the Status - /// - public string Status { get; } - - /// - /// Constructor for WebViewStatusChangedEventArgs - /// - /// the new status of the webview - public WebViewStatusChangedEventArgs(string status) + public interface IHasWebView { - Status = status; + /// + /// Indicates whether the webview is currently visible + /// + bool WebviewIsVisible { get; } + + /// + /// Shows the webview with the specified parameters + /// + /// the URL to display in the webview + /// the display mode for the webview + /// the title to display on the webview + /// the target for the webview + void ShowWebView(string url, string mode, string title, string target); + + /// + /// Hides the webview + /// + void HideWebView(); + + /// + /// Event raised when the webview status changes + /// + event EventHandler WebViewStatusChanged; + + } + + + /// + /// Defines the contract for IHasWebViewWithPwaMode + /// + public interface IHasWebViewWithPwaMode : IHasWebView + { + /// + /// Indicates whether the webview is currently in PWA mode + /// + bool IsInPwaMode { get; } + + /// + /// Gets the BoolFeedback indicating whether the webview is currently in PWA mode + /// + BoolFeedback IsInPwaModeFeedback { get; } + + /// + /// Sends navigators to the specified PWA URL. Accepts an absolute URL or a relative URL for a mobile control app + /// + /// The URL to navigate to + void SendNavigatorsToPwaUrl(string url); + + /// + /// Exits navigators from PWA mode + /// + void ExitNavigatorsPwaMode(); + } + + + /// + /// Represents a WebViewStatusChangedEventArgs + /// + public class WebViewStatusChangedEventArgs : EventArgs + { + /// + /// Gets or sets the Status + /// + public string Status { get; } + + /// + /// Gets or sets the WebViewEvent associated with the status change, which can provide additional information about the webview event that triggered the status change, such as display parameters or error information. This property allows you to include the full WebViewEvent in the event args, giving you access to all relevant details about the webview event when handling the WebViewStatusChanged event. + /// + public WebViewEvent WebView { get; } + + /// + /// Constructor for WebViewStatusChangedEventArgs + /// + /// the new status of the webview + public WebViewStatusChangedEventArgs(string status) + { + Status = status; + } + + /// + /// Constructor for WebViewStatusChangedEventArgs with WebViewEvent parameter, which can provide additional information about the webview event that triggered the status change, such as display parameters or error information. This constructor allows you to include the full WebViewEvent in the event args, giving you access to all relevant details about the webview event when handling the WebViewStatusChanged event. + /// + /// the new status of the webview + /// the WebViewEvent associated with the status change + public WebViewStatusChangedEventArgs(string status, WebViewEvent webview) + { + Status = status; + WebView = webview; + } } } - From 695ab00c6ebd266b2d8f54ccb630f44874ea6199 Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Tue, 25 Aug 2026 17:24:43 -0400 Subject: [PATCH 156/161] fix: correct malformed error message --- src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs index e3591297..74796da1 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -216,7 +216,7 @@ public class DeviceFactory } catch (Exception ex) { - Debug.LogError(ex, "Exception occurred while creating device {0}: {1}", null, dc.Key, ex.Message); + Debug.LogError(ex, "Exception occurred while creating device {deviceKey}: {message}", dc.Key, ex.Message); return null; } } From 4926b005d634898b25787d2822b285f1a1444c4d Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Tue, 25 Aug 2026 17:30:34 -0400 Subject: [PATCH 157/161] fix: use continue instead of return when a touchpanel room bridge is missing, and register room bridges added via AddDefaultDeviceMessenger --- .../MobileControlSystemController.cs | 6 ++++++ .../WebSocketServer/MobileControlWebsocketServer.cs | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index ea3b02d1..c0584c65 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -512,6 +512,12 @@ namespace PepperDash.Essentials { simplMessenger.ConfigurationIsReady += Bridge_ConfigurationIsReady; } + + if (messenger is MobileControlBridgeBase roomBridge) + { + _roomBridges.Add(roomBridge); + } + this.LogVerbose( "Adding default messenger with key {messengerKey} for path {messengerPath}", messenger.Key, diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 0f03f2a9..ba6a94fb 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -356,11 +356,10 @@ namespace PepperDash.Essentials.WebSocketServer foreach (var client in touchpanelsToAdd) { var bridge = _parent.GetRoomBridge(client.DefaultRoomKey); - if (bridge == null) { this.LogWarning("Unable to find room with key: {defaultRoomKey}", client.DefaultRoomKey); - return; + continue; } var (key, path) = GenerateClientToken(bridge, client.Key); From 592ece5a3dd40b80e7bc52ef6ef7fb0892496639 Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Tue, 25 Aug 2026 17:49:26 -0400 Subject: [PATCH 158/161] fix: route room-scoped default messengers under /room/{key} to match client request routing --- .../MobileControlSystemController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index c0584c65..c2c080fc 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -286,7 +286,10 @@ namespace PepperDash.Essentials if (!entry.Matches(device)) continue; - var messenger = entry.Factory(device, $"/device/{device.Key}", Key); + // Room-scoped messengers must share the room bridge's "/room/{key}" path so client requests route correctly + var basePath = device is IEssentialsRoom ? $"/room/{device.Key}" : $"/device/{device.Key}"; + + var messenger = entry.Factory(device, basePath, Key); if (messenger == null) continue; From e0f7fc9a6ef6cd0aad61504756ee774185590151 Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Wed, 26 Aug 2026 15:23:58 -0400 Subject: [PATCH 159/161] feat: add IHasNamedRoutingSlots interface and messenger for matrix routing status --- .../Routing/IHasNamedRoutingSlots.cs | 49 +++++++++++ .../IHasNamedRoutingSlotsMessenger.cs | 85 +++++++++++++++++++ .../MessengerFactoryRegistry.cs | 8 ++ 3 files changed, 142 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/Routing/IHasNamedRoutingSlots.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasNamedRoutingSlotsMessenger.cs diff --git a/src/PepperDash.Essentials.Core/Routing/IHasNamedRoutingSlots.cs b/src/PepperDash.Essentials.Core/Routing/IHasNamedRoutingSlots.cs new file mode 100644 index 00000000..84b5983e --- /dev/null +++ b/src/PepperDash.Essentials.Core/Routing/IHasNamedRoutingSlots.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using PepperDash.Core; + +namespace PepperDash.Essentials.Core; + +/// +/// Describes a single named routing slot (input or output) on a device implementing +/// . +/// +public interface IRoutingSlotInfo : IKeyName +{ + /// Matrix slot number. + int SlotNumber { get; } + + /// Signal types this slot can carry. + eRoutingSignalType SupportedSignalTypes { get; } +} + +/// +/// Describes a named output slot, adding per-signal-type current-route feedback (keyed by the +/// routed input slot's key) that does not +/// carry. +/// +public interface IRoutingOutputSlotInfo : IRoutingSlotInfo +{ + /// The key of the input slot currently routed to this output, per signal type. + IReadOnlyDictionary CurrentRouteInputKeys { get; } + + /// Raised when the routed input on this output changes. + event EventHandler OutputSlotChanged; +} + +/// +/// Optional extension to for matrix-style routing +/// devices that track named input/output slots (with per-signal-type routing feedback) internally, +/// e.g. plugin-local slot abstractions that replaced the removed core IRoutingInputSlot/ +/// IRoutingOutputSlot during the v3 routing refactor. Devices implementing this get a richer +/// mobile-control matrix routing message (names + per-signal-type feedback) instead of the bare +/// key-only ports from alone. +/// +public interface IHasNamedRoutingSlots : IRoutingMidpointWithFeedback +{ + /// Named input slots, keyed by slot key. + IReadOnlyDictionary InputSlots { get; } + + /// Named output slots, keyed by slot key. + IReadOnlyDictionary OutputSlots { get; } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasNamedRoutingSlotsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasNamedRoutingSlotsMessenger.cs new file mode 100644 index 00000000..855a4aa7 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasNamedRoutingSlotsMessenger.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Messenger for devices implementing — sends named + /// input/output slots with per-signal-type current-route feedback, which the bare + /// (registered for every + /// device, including this one) cannot provide. + /// + public class IHasNamedRoutingSlotsMessenger : MessengerBase + { + private readonly IHasNamedRoutingSlots _device; + + public IHasNamedRoutingSlotsMessenger(string key, string messagePath, IHasNamedRoutingSlots device) + : base(key, messagePath, device as IKeyName) + { + _device = device; + } + + protected override void RegisterActions() + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus(id)); + + foreach (var output in _device.OutputSlots.Values) + { + output.OutputSlotChanged += (sender, args) => SendFullStatus(); + } + } + + private static RoutingSlotMessage BuildSlotMessage(IRoutingSlotInfo slot) => new RoutingSlotMessage + { + Key = slot.Key, + Name = slot.Name, + SlotNumber = slot.SlotNumber, + SupportedSignalTypes = slot.SupportedSignalTypes.ToString() + }; + + private void SendFullStatus(string id = null) + { + var inputSlots = _device.InputSlots.ToDictionary(kvp => kvp.Key, kvp => BuildSlotMessage(kvp.Value)); + + var outputSlots = _device.OutputSlots.ToDictionary(kvp => kvp.Key, kvp => + { + var message = BuildSlotMessage(kvp.Value); + message.CurrentRouteInputKeys = kvp.Value.CurrentRouteInputKeys + .ToDictionary(r => r.Key.ToString(), r => r.Value); + return message; + }); + + var content = JToken.FromObject(new + { + inputSlots, + outputSlots + }); + + PostStatusMessage(content, MessagePath, id); + } + } + + public class RoutingSlotMessage + { + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("slotNumber")] + public int SlotNumber { get; set; } + + [JsonProperty("supportedSignalTypes")] + public string SupportedSignalTypes { get; set; } + + [JsonProperty("currentRouteInputKeys", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary CurrentRouteInputKeys { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs index d2b5c73e..29ede86d 100644 --- a/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs +++ b/src/PepperDash.Essentials.MobileControl/MessengerFactoryRegistry.cs @@ -374,6 +374,14 @@ namespace PepperDash.Essentials (d, mp, _) => new IRoutingMidpointWithFeedbackMessenger( $"{d.Key}-matrixRouting", mp, (IRoutingMidpointWithFeedback)d) ), + // Devices that also expose named slots (plugin-local IDmInputSlot/INvxInputSlot- + // style abstractions) get a second, richer message with names + per-signal-type + // route feedback the bare messenger above cannot provide. + new MessengerFactoryEntry( + typeof(IHasNamedRoutingSlots), + (d, mp, _) => new IHasNamedRoutingSlotsMessenger( + $"{d.Key}-namedRoutingSlots", mp, (IHasNamedRoutingSlots)d) + ), // ── Environmental sensors ───────────────────────────────────────────────── // Preserving original key format (no controller key suffix) From cfd07eee9c88bd44d4cdefdaae1f85a6fd8de190 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 26 Aug 2026 20:38:11 -0600 Subject: [PATCH 160/161] feat: add MockAudioDevice and factory for audio simulation --- .../Audio/MockAudioDevice.cs | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/PepperDash.Essentials.Devices.Common/Audio/MockAudioDevice.cs diff --git a/src/PepperDash.Essentials.Devices.Common/Audio/MockAudioDevice.cs b/src/PepperDash.Essentials.Devices.Common/Audio/MockAudioDevice.cs new file mode 100644 index 00000000..e368a521 --- /dev/null +++ b/src/PepperDash.Essentials.Devices.Common/Audio/MockAudioDevice.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using System.Timers; + +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using Serilog.Events; + +namespace PepperDash.Essentials.Devices.Common; + +/// +/// Mock audio device for testing and simulation purposes. Behaves like a real DSP fader channel, +/// with ramping volume up/down while pressed and mute on/off/toggle, all backed by fake in-memory +/// state rather than any real hardware communication. +/// +public class MockAudioDevice : EssentialsDevice, IBasicVolumeWithFeedback +{ + private const int VolumeHeldRepeatIntervalMs = 100; + private const ushort VolumeStep = 655; + + private ushort _volumeLevel = 32768; + private bool _isMuted; + private Timer _volumeUpTimer; + private Timer _volumeDownTimer; + + /// + public IntFeedback VolumeLevelFeedback { get; private set; } + + /// + public BoolFeedback MuteFeedback { get; private set; } + + /// + /// Constructor for MockAudioDevice + /// + /// Device key + /// Device name + public MockAudioDevice(string key, string name) + : base(key, name) + { + VolumeLevelFeedback = new IntFeedback("volume", () => _volumeLevel); + MuteFeedback = new BoolFeedback("muteOn", () => _isMuted); + + // Seed the feedback's cached value immediately so the UI reflects the starting level + // rather than 0 until the first volume change fires an update. + VolumeLevelFeedback.FireUpdate(); + MuteFeedback.FireUpdate(); + } + + /// + public void SetVolume(ushort level) + { + _volumeLevel = level; + VolumeLevelFeedback.InvokeFireUpdate(); + this.LogDebug("SetVolume: {Level}", _volumeLevel); + } + + /// + public void MuteOn() + { + _isMuted = true; + MuteFeedback.InvokeFireUpdate(); + } + + /// + public void MuteOff() + { + _isMuted = false; + MuteFeedback.InvokeFireUpdate(); + } + + /// + public void MuteToggle() + { + if (_isMuted) + MuteOff(); + else + MuteOn(); + } + + /// + public void VolumeUp(bool pressRelease) + { + if (pressRelease) + { + RampVolume(VolumeStep); + + if (_volumeUpTimer == null) + { + _volumeUpTimer = new Timer(VolumeHeldRepeatIntervalMs) { AutoReset = true }; + _volumeUpTimer.Elapsed += (s, e) => RampVolume(VolumeStep); + _volumeUpTimer.Start(); + } + } + else + { + _volumeUpTimer?.Stop(); + _volumeUpTimer = null; + } + } + + /// + public void VolumeDown(bool pressRelease) + { + if (pressRelease) + { + RampVolume(-VolumeStep); + + if (_volumeDownTimer == null) + { + _volumeDownTimer = new Timer(VolumeHeldRepeatIntervalMs) { AutoReset = true }; + _volumeDownTimer.Elapsed += (s, e) => RampVolume(-VolumeStep); + _volumeDownTimer.Start(); + } + } + else + { + _volumeDownTimer?.Stop(); + _volumeDownTimer = null; + } + } + + // Clamps to ushort range so repeated ramping at the extremes doesn't wrap around. + private void RampVolume(int delta) + { + var newLevel = _volumeLevel + delta; + if (newLevel < ushort.MinValue) + newLevel = ushort.MinValue; + else if (newLevel > ushort.MaxValue) + newLevel = ushort.MaxValue; + + SetVolume((ushort)newLevel); + } +} + +/// +/// Factory for building devices. +/// +public class MockAudioDeviceFactory : EssentialsDeviceFactory +{ + /// + /// Initializes a new instance of the class. + /// + public MockAudioDeviceFactory() + { + TypeNames = new List { "mockaudiodevice", "mockaudio" }; + } + + /// + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + Debug.LogMessage(LogEventLevel.Debug, "Factory attempting to create new Mock Audio Device"); + return new MockAudioDevice(dc.Key, dc.Name); + } +} From fec59d2149a6036879c574dc8a44b727402c709f Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Thu, 27 Aug 2026 08:58:19 -0400 Subject: [PATCH 161/161] feat(mock-routing): add IHasNamedRoutingSlots support to MockRoutingMidpoint --- .../Routing/MockRoutingMidpoint.cs | 126 +++++++++++++++++- .../MockRoutingMidpointPropertiesConfig.cs | 8 ++ 2 files changed, 127 insertions(+), 7 deletions(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs index 74853223..8a6ef2f6 100644 --- a/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs +++ b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpoint.cs @@ -11,14 +11,15 @@ using Serilog.Events; namespace PepperDash.Essentials.Devices.Common.Routing; /// -/// A mock midpoint routing device (e.g. a matrix switcher) that implements +/// A mock midpoint routing device (e.g. a matrix switcher) that implements /// without any real hardware communication. Its input and output ports are configured via /// , each with a name, signal type, and physical port -/// (connection) type - so it can stand in for a real switching device (such as a StreamSync matrix) for -/// development and testing of routing logic. +/// (connection) type - so it can stand in for a real switching device (such as a DM chassis or a StreamSync +/// matrix) for development and testing of routing logic, including named-routing-slot UI that a bare +/// device cannot support. /// [Description("A mock routing midpoint (e.g. matrix switcher) device for testing routing logic without real hardware")] -public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedback +public class MockRoutingMidpoint : EssentialsDevice, IHasNamedRoutingSlots { /// /// The configuration properties for this device. @@ -37,6 +38,14 @@ public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedbac /// public event RouteChangedEventHandler RouteChanged; + private readonly Dictionary _outputSlotsByKey = new Dictionary(); + + /// + public IReadOnlyDictionary InputSlots { get; private set; } + + /// + public IReadOnlyDictionary OutputSlots { get; private set; } + /// /// Initializes a new instance of the class from a . /// @@ -56,14 +65,20 @@ public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedbac } /// - /// Builds the input and output ports from . Each port's Key and Selector - /// are both set to the configured port name, so selectors passed to and - /// can simply be looked up by matching against the port's Selector. + /// Builds the input and output ports (and their slot info) from + /// . Each port's Key and Selector are both set to the configured port + /// name, so selectors passed to and can simply be + /// looked up by matching against the port's Selector. Slot number is the 1-based position of the port + /// within its input/output list. /// private void BuildPorts() { try { + var inputSlots = new Dictionary(); + var outputSlots = new Dictionary(); + var slotNumber = 0; + foreach (var portConfig in PropertiesConfig.InputPorts) { if (string.IsNullOrEmpty(portConfig.Name)) @@ -74,8 +89,13 @@ public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedbac var port = new RoutingInputPort(portConfig.Name, portConfig.SignalType, portConfig.PortType, portConfig.Name, this); InputPorts.Add(port); + + slotNumber++; + inputSlots[portConfig.Name] = new MockRoutingSlotInfo( + portConfig.Name, portConfig.Label ?? portConfig.Name, slotNumber, portConfig.SignalType); } + slotNumber = 0; foreach (var portConfig in PropertiesConfig.OutputPorts) { if (string.IsNullOrEmpty(portConfig.Name)) @@ -86,8 +106,17 @@ public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedbac var port = new RoutingOutputPort(portConfig.Name, portConfig.SignalType, portConfig.PortType, portConfig.Name, this); OutputPorts.Add(port); + + slotNumber++; + var outputSlot = new MockRoutingOutputSlotInfo( + portConfig.Name, portConfig.Label ?? portConfig.Name, slotNumber, portConfig.SignalType); + _outputSlotsByKey[portConfig.Name] = outputSlot; + outputSlots[portConfig.Name] = outputSlot; } + InputSlots = inputSlots; + OutputSlots = outputSlots; + this.LogInformation("Built {inputCount} input port(s) and {outputCount} output port(s) for mock midpoint {key}", InputPorts.Count, OutputPorts.Count, Key); } @@ -121,6 +150,11 @@ public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedbac { this.LogInformation("Clearing route to output {output} on {key}", outputPort.Key, Key); + if (_outputSlotsByKey.TryGetValue(outputPort.Key, out var clearedSlot)) + { + clearedSlot.ClearRoute(signalType); + } + var clearedDescriptor = new RouteSwitchDescriptor(outputPort, null); RouteChanged?.Invoke(this, clearedDescriptor); return; @@ -137,6 +171,11 @@ public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedbac var descriptor = new RouteSwitchDescriptor(outputPort, inputPort); CurrentRoutes.Add(descriptor); + if (_outputSlotsByKey.TryGetValue(outputPort.Key, out var routedSlot)) + { + routedSlot.SetRoute(signalType, inputPort.Key); + } + this.LogInformation("Executed switch: {input} -> {output} ({signalType}) on {key}", inputPort.Key, outputPort.Key, signalType, Key); @@ -155,6 +194,79 @@ public class MockRoutingMidpoint : EssentialsDevice, IRoutingMidpointWithFeedbac } } +/// +/// Named routing slot info for a input or output port. +/// +class MockRoutingSlotInfo : IRoutingSlotInfo +{ + /// + public string Key { get; } + + /// + public string Name { get; } + + /// + public int SlotNumber { get; } + + /// + public eRoutingSignalType SupportedSignalTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + public MockRoutingSlotInfo(string key, string name, int slotNumber, eRoutingSignalType supportedSignalTypes) + { + Key = key; + Name = name; + SlotNumber = slotNumber; + SupportedSignalTypes = supportedSignalTypes; + } +} + +/// +/// Named output routing slot info for a output port, tracking the +/// currently routed input key per signal type since the mock's flat +/// list does not carry signal type. +/// +class MockRoutingOutputSlotInfo : MockRoutingSlotInfo, IRoutingOutputSlotInfo +{ + private readonly Dictionary _currentRouteInputKeys = new Dictionary(); + + /// + public IReadOnlyDictionary CurrentRouteInputKeys => _currentRouteInputKeys; + + /// + public event EventHandler OutputSlotChanged; + + /// + /// Initializes a new instance of the class. + /// + public MockRoutingOutputSlotInfo(string key, string name, int slotNumber, eRoutingSignalType supportedSignalTypes) + : base(key, name, slotNumber, supportedSignalTypes) + { + } + + /// + /// Records the input key routed to this output for the given signal type and raises . + /// + public void SetRoute(eRoutingSignalType signalType, string inputKey) + { + _currentRouteInputKeys[signalType] = inputKey; + OutputSlotChanged?.Invoke(this, EventArgs.Empty); + } + + /// + /// Clears the routed input key for the given signal type and raises if it changed. + /// + public void ClearRoute(eRoutingSignalType signalType) + { + if (_currentRouteInputKeys.Remove(signalType)) + { + OutputSlotChanged?.Invoke(this, EventArgs.Empty); + } + } +} + /// /// Factory for building devices. /// diff --git a/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs index 7c7d4bc3..b24e31aa 100644 --- a/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Devices.Common/Routing/MockRoutingMidpointPropertiesConfig.cs @@ -50,4 +50,12 @@ public class MockRoutingMidpointPortConfig [JsonProperty("portType")] [JsonConverter(typeof(StringEnumConverter))] public eRoutingPortConnectionType PortType { get; set; } = eRoutingPortConnectionType.Hdmi; + + /// + /// Optional friendly display name for this port (e.g. "Rm A Codec"), surfaced via + /// for named-routing-slot UI. Defaults to if + /// not specified. + /// + [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] + public string Label { get; set; } }