From 8db559f1975115607d08d59825db91224f0226d9 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 08:28:55 -0500 Subject: [PATCH 01/12] feat: factory updates & refactoring This commit introduces significant updates to the device factory system, enhancing the way devices are created and managed within the PepperDash Essentials framework. The changes include: - New attributes for device configuration and description. - Refactoring of the device manager and essentials device classes to support new factory methods. - modified factory classes for essentials devices, plugin development devices, and processor extension devices. - The device factory interface has been updated to include a factory method for creating devices. - Added a wrapper for the device factory to streamline device creation. - Updated plugin loader to accommodate the new device factory structure. Fixes #1065 Fixed #1277 --- .../Devices/ConfigSnippetAttribute.cs | 33 +++ .../Devices/DescriptionAttribute.cs | 32 +++ .../Devices/DeviceManager.cs | 70 ++--- .../Devices/EssentialsDevice.cs | 160 ++--------- .../Devices/EssentialsDeviceFactory.cs | 27 ++ ...ssentialsPluginDevelopmentDeviceFactory.cs | 18 ++ .../Devices/EssentialsPluginDeviceFactory.cs | 14 + .../ProcessorExtensionDeviceFactory.cs | 41 +++ .../Factory/DeviceFactory.cs | 251 +++++++++++------- .../Factory/DeviceFactoryWrapper.cs | 43 +++ .../Factory/IDeviceFactory.cs | 23 +- .../Plugins/IPluginDeviceFactory.cs | 10 +- .../Plugins/PluginLoader.cs | 111 ++++++-- .../DeviceFactory.cs | 23 +- .../MobileControlFactory.cs | 27 +- src/PepperDash.Essentials/ControlSystem.cs | 132 ++++----- .../Factory/DeviceFactory.cs | 26 +- 17 files changed, 662 insertions(+), 379 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/EssentialsPluginDevelopmentDeviceFactory.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/EssentialsPluginDeviceFactory.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs create mode 100644 src/PepperDash.Essentials.Core/Factory/DeviceFactoryWrapper.cs diff --git a/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs b/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs new file mode 100644 index 00000000..76d3640c --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs @@ -0,0 +1,33 @@ +using System; + +namespace PepperDash.Essentials.Core +{ + /// + /// Represents a ConfigSnippetAttribute + /// + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] + public class ConfigSnippetAttribute : Attribute + { + private string _ConfigSnippet; + + /// + /// Represents a configuration snippet for the device. + /// + /// + public ConfigSnippetAttribute(string configSnippet) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Setting Config Snippet {0}", 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 { return _ConfigSnippet; } + } + } + +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs b/src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs new file mode 100644 index 00000000..6d4073d9 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs @@ -0,0 +1,32 @@ +using System; + +namespace PepperDash.Essentials.Core +{ + /// + /// Represents a description attribute for a device. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] + public class DescriptionAttribute : Attribute + { + private string _Description; + + /// + /// Represents a description attribute for a device. + /// + /// + public DescriptionAttribute(string description) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Setting Description: {0}", description); + _Description = description; + } + + /// + /// Gets the description for the device. + /// + public string Description + { + get { return _Description; } + } + } + +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index fd116aed..7572835e 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -1,26 +1,38 @@ -using Crestron.SimplSharp; -using Crestron.SimplSharpPro; -using PepperDash.Core; -using Serilog.Events; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharpPro; +using PepperDash.Core; +using Serilog.Events; namespace PepperDash.Essentials.Core { + /// + /// Manages the devices in the system + /// public static class DeviceManager { + /// + /// Raised when all devices have been activated + /// public static event EventHandler AllDevicesActivated; + + /// + /// Raised when all devices have been registered + /// public static event EventHandler AllDevicesRegistered; + + /// + /// Raised when all devices have been initialized + /// public static event EventHandler AllDevicesInitialized; private static readonly CCriticalSection DeviceCriticalSection = new CCriticalSection(); - private static readonly CEvent AllowAddDevicesCEvent = new CEvent(false, true); - //public static List Devices { get { return _Devices; } } - //static List _Devices = new List(); + private static readonly CEvent AllowAddDevicesCEvent = new CEvent(false, true); private static readonly Dictionary Devices = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -74,7 +86,7 @@ namespace PepperDash.Essentials.Core foreach (var d in Devices.Values) { try - { + { if (d is Device) (d as Device).PreActivate(); } @@ -188,27 +200,6 @@ namespace PepperDash.Essentials.Core } } - //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); @@ -397,6 +388,25 @@ namespace PepperDash.Essentials.Core 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 /// diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs index 2fcbf1a4..59fba681 100644 --- a/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs @@ -1,12 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using System.Reflection; - +using System.Threading.Tasks; using PepperDash.Core; -using PepperDash.Essentials.Core.Config; using Serilog.Events; namespace PepperDash.Essentials.Core @@ -17,9 +11,16 @@ namespace PepperDash.Essentials.Core [Description("The base Essentials Device Class")] public abstract class EssentialsDevice : Device { + /// + /// 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; } @@ -36,12 +37,21 @@ namespace PepperDash.Essentials.Core } } + /// + /// 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) { @@ -55,7 +65,7 @@ namespace PepperDash.Essentials.Core private void DeviceManagerOnAllDevicesActivated(object sender, EventArgs eventArgs) { - CrestronInvoke.BeginInvoke((o) => + Task.Run(() => { try { @@ -90,138 +100,4 @@ 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)] - /// - /// Represents a ConfigSnippetAttribute - /// - 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; } - } - } - - /// - /// Devices the basic needs for a Device Factory - /// - public abstract class EssentialsDeviceFactory : IDeviceFactory where T:EssentialsDevice - { - #region IDeviceFactory 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; } - - /// - /// LoadTypeFactories method - /// - public void LoadTypeFactories() - { - 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[]; - DeviceFactory.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 - } - - 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) - { - //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; } - } - - 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/EssentialsDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs new file mode 100644 index 00000000..3ac326bf --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsDeviceFactory.cs @@ -0,0 +1,27 @@ +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 + /// + 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); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDevelopmentDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDevelopmentDeviceFactory.cs new file mode 100644 index 00000000..62864551 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDevelopmentDeviceFactory.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace PepperDash.Essentials.Core +{ + 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 new file mode 100644 index 00000000..7a891905 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/EssentialsPluginDeviceFactory.cs @@ -0,0 +1,14 @@ +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 new file mode 100644 index 00000000..469d18ec --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Core +{ + 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) + { + //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 + + } + +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs index 5c8d4dec..08aa67cf 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -1,123 +1,162 @@  -using Crestron.SimplSharp; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Reflection; +using Crestron.SimplSharp; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; -using System; -using System.Collections.Generic; -using System.Linq; namespace PepperDash.Essentials.Core { /// - /// Wrapper class for device factory information - /// - public class DeviceFactoryWrapper - { - /// - /// Gets or sets the device type - /// - public Type Type { get; set; } - - /// - /// Gets or sets the Description - /// - public string Description { get; set; } - - /// - /// Gets or sets the factory method for creating devices - /// - public Func FactoryMethod { get; set; } - - /// - /// Initializes a new instance of the DeviceFactoryWrapper class - /// - public DeviceFactoryWrapper() - { - Type = null; - Description = "Not Available"; - } - } - - /// - /// Represents a DeviceFactory + /// 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 { /// - /// Initializes a new instance of the DeviceFactory class and loads all device type factories + /// 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 assy = Assembly.GetExecutingAssembly(); - PluginLoader.SetEssentialsAssembly(assy.GetName().Name, assy); + var programAssemblies = Directory.GetFiles(InitialParametersClass.ProgramDirectory.ToString(), "*.dll"); - var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); - - if (types != null) + foreach (var assembly in programAssemblies) { + 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); + + PluginLoader.AddLoadedAssembly(assembly.GetName().Name, 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); - factory.LoadTypeFactories(); + LoadDeviceFactories(factory); } catch (Exception e) { - Debug.LogMessage(LogEventLevel.Information, "Unable to load type: '{1}' DeviceFactory: {0}", e, type.Name); + Debug.LogError("Unable to load type: '{message}' DeviceFactory: {type}", e.Message, type.Name); } } + } } - /// - /// 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 FactoryMethods = + /// + /// 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[]; + 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); - /// - /// Adds a plugin factory method - /// - /// - /// - public static void AddFactoryForType(string typeName, Func method) - { - //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); - DeviceFactory.FactoryMethods.Add(typeName, new DeviceFactoryWrapper() { FactoryMethod = method}); - } + /// + /// 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) { - //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); - - if(FactoryMethods.ContainsKey(typeName)) + if (FactoryMethods.ContainsKey(typeName)) { - Debug.LogMessage(LogEventLevel.Information, "Unable to add type: '{0}'. Already exists in DeviceFactory", typeName); + Debug.LogInformation("Unable to add type: '{typeName}'. Already exists in DeviceFactory", typeName); return; } var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; - DeviceFactory.FactoryMethods.Add(typeName, wrapper); + + 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.ToLower() == "secret") + if (prop.Name.Equals("secret", StringComparison.CurrentCultureIgnoreCase)) { 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 JObject; - if (recurseProp == null) return; + + if (!(prop.Value is JObject recurseProp)) return; + CheckForSecrets(recurseProp.Properties()); } } @@ -127,66 +166,70 @@ namespace PepperDash.Essentials.Core 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); - return String.Empty; + return string.Empty; } /// - /// The factory method for Core "things". Also iterates the Factory methods that have - /// been loaded from plugins - /// - /// - /// - /// - /// GetDevice method + /// 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 { - 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 JObject; - if (jObject != null) + if (properties is JObject jObject) { var jProp = jObject.Properties(); CheckForSecrets(jProp); } - Debug.LogMessage(LogEventLevel.Verbose, "typeName = {0}", typeName); - // Check for types that have been added by plugin dlls. - return !FactoryMethods.ContainsKey(typeName) ? null : FactoryMethods[typeName].FactoryMethod(localDc); + 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.LogMessage(ex, "Exception occurred while creating device {0}: {1}", null, dc.Key, ex.Message); + Debug.LogError(ex, "Exception occurred while creating device {0}: {1}", null, dc.Key, ex.Message); return null; } } /// - /// Prints the type names and associated metadata from the FactoryMethods collection. - /// - /// - /// - /// GetDeviceFactoryTypes method + /// 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) @@ -212,16 +255,18 @@ namespace PepperDash.Essentials.Core } } - /// - /// Returns the device factory dictionary - /// - /// - /// - 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); - } + /// + /// 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); + } } -} \ No newline at end of file +} diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactoryWrapper.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactoryWrapper.cs new file mode 100644 index 00000000..dc1f72d9 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactoryWrapper.cs @@ -0,0 +1,43 @@ + + +using System; +using PepperDash.Core; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Core +{ + /// + /// Wraps a device factory, providing metadata and a factory method for creating devices. + /// + public class DeviceFactoryWrapper + { + /// + /// Gets or sets the type associated with the current instance. + /// + public Type Type { get; set; } + + /// + /// Gets or sets the description associated with the object. + /// + public string Description { get; set; } + + /// + /// Gets or sets the factory method used to create an instance based on the provided . + /// + /// The factory method allows customization of how instances are created for + /// specific inputs. Ensure the delegate is not null before invoking it. + public Func FactoryMethod { get; set; } + + /// + /// Initializes a new instance of the class with default values. + /// + /// The property is initialized to , and the property is set to "Not Available". + public DeviceFactoryWrapper() + { + Type = null; + Description = "Not Available"; + } + } +} diff --git a/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs index 31ff95d2..b267edf6 100644 --- a/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/IDeviceFactory.cs @@ -1,4 +1,8 @@ -namespace PepperDash.Essentials.Core +using System; +using System.Collections.Generic; +using PepperDash.Essentials.Core.Config; + +namespace PepperDash.Essentials.Core { /// /// Defines the contract for IDeviceFactory @@ -6,8 +10,21 @@ public interface IDeviceFactory { /// - /// Loads all the types to the DeviceFactory + /// Gets the type of the factory associated with the current instance. /// - void LoadTypeFactories(); + Type FactoryType { 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); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs index ec823007..caad4b57 100644 --- a/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Plugins/IPluginDeviceFactory.cs @@ -1,5 +1,5 @@ +using System; using System.Collections.Generic; -using PepperDash.Core; namespace PepperDash.Essentials.Core @@ -16,8 +16,16 @@ namespace PepperDash.Essentials.Core } + /// + /// Defines a class that is capable of loading custom plugin device types for development purposes + /// + [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.")] public interface IPluginDevelopmentDeviceFactory : IPluginDeviceFactory { + /// + /// Gets a list of all the development versions of the Essentials framework that are supported by this factory. + /// List DevelopmentEssentialsFrameworkVersions { get; } } } diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index a74ba16a..8978c752 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -1,15 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using Crestron.SimplSharp; -// using Crestron.SimplSharp.CrestronIO; using System.Reflection; - +using Crestron.SimplSharp; +using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; -using Newtonsoft.Json; -using System.IO; namespace PepperDash.Essentials { @@ -28,10 +26,19 @@ namespace PepperDash.Essentials /// static List LoadedPluginFolderAssemblies; + /// + /// The assembly for the Essentials Framework + /// public static LoadedAssembly EssentialsAssembly { get; private set; } + /// + /// The assembly for the PepperDash Core + /// public static LoadedAssembly PepperDashCoreAssembly { get; private set; } + /// + /// The list of assemblies loaded from the Essentials plugins folder + /// public static List EssentialsPluginAssemblies { get; private set; } /// @@ -131,7 +138,7 @@ namespace PepperDash.Essentials /// /// Loads an assembly via Reflection and adds it to the list of loaded assemblies /// - /// + /// static LoadedAssembly LoadAssembly(string filePath) { try @@ -153,7 +160,8 @@ namespace PepperDash.Essentials } return null; - } catch(Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath); return null; @@ -212,6 +220,20 @@ namespace PepperDash.Essentials } } + /// + /// 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 /// @@ -402,7 +424,7 @@ namespace PepperDash.Essentials try { var assy = loadedAssembly.Assembly; - Type[] types = {}; + Type[] types = { }; try { types = assy.GetTypes(); @@ -419,8 +441,8 @@ namespace PepperDash.Essentials foreach (var type in types) { try - { - if (typeof (IPluginDeviceFactory).IsAssignableFrom(type) && !type.IsAbstract) + { + if (typeof(IPluginDeviceFactory).IsAssignableFrom(type) && !type.IsAbstract) { var plugin = (IPluginDeviceFactory)Activator.CreateInstance(type); @@ -454,37 +476,61 @@ namespace PepperDash.Essentials } /// - /// Loads a + /// Loads a custom plugin and performs a dependency check to ensure compatibility with the required Essentials + /// framework version. /// - /// - /// - static void LoadCustomPlugin(IPluginDeviceFactory plugin, LoadedAssembly 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. + static void LoadCustomPlugin(IPluginDeviceFactory pluginDeviceFactory, LoadedAssembly loadedAssembly) { - var developmentPlugin = plugin as IPluginDevelopmentDeviceFactory; - - var passed = developmentPlugin != null ? Global.IsRunningDevelopmentVersion - (developmentPlugin.DevelopmentEssentialsFrameworkVersions, developmentPlugin.MinimumEssentialsFrameworkVersion) - : Global.IsRunningMinimumVersionOrHigher(plugin.MinimumEssentialsFrameworkVersion); + 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********************", - plugin.MinimumEssentialsFrameworkVersion, loadedAssembly.Name); + pluginDeviceFactory.MinimumEssentialsFrameworkVersion, loadedAssembly.Name); return; } else { - Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", plugin.MinimumEssentialsFrameworkVersion); + Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", pluginDeviceFactory.MinimumEssentialsFrameworkVersion); } - Debug.LogMessage(LogEventLevel.Information, "Loading plugin: {0}", loadedAssembly.Name); - plugin.LoadTypeFactories(); + Debug.LogMessage(LogEventLevel.Information, "Loading plugin factory: {0}", loadedAssembly.Name); - if(!EssentialsPluginAssemblies.Contains(loadedAssembly)) + 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) + { + //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 a a custom plugin via the legacy method /// @@ -567,13 +613,30 @@ namespace PepperDash.Essentials /// public class LoadedAssembly { + /// + /// Gets the name of the assembly + /// [JsonProperty("name")] public string Name { get; private set; } + + /// + /// 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) { Name = name; diff --git a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs index 7656b277..a0fa0c9f 100644 --- a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs @@ -19,7 +19,7 @@ namespace PepperDash.Essentials.Devices.Common { var assy = Assembly.GetExecutingAssembly(); PluginLoader.SetEssentialsAssembly(assy.GetName().Name, assy); - + var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); if (types != null) @@ -29,7 +29,7 @@ namespace PepperDash.Essentials.Devices.Common try { var factory = (IDeviceFactory)Activator.CreateInstance(type); - factory.LoadTypeFactories(); + LoadDeviceFactories(factory); } catch (Exception e) { @@ -38,5 +38,24 @@ namespace PepperDash.Essentials.Devices.Common } } } + + /// + /// 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); + } + } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index 7d4b0340..ce40aa64 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -1,8 +1,8 @@ -using PepperDash.Core; -using PepperDash.Essentials.Core; -using System; +using System; using System.Linq; using System.Reflection; +using PepperDash.Core; +using PepperDash.Essentials.Core; namespace PepperDash.Essentials { @@ -30,7 +30,7 @@ namespace PepperDash.Essentials { var factory = (IDeviceFactory)Activator.CreateInstance(type); - factory.LoadTypeFactories(); + LoadDeviceFactories(factory); } catch (Exception ex) { @@ -38,5 +38,24 @@ namespace PepperDash.Essentials } } } + + /// + /// 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 56615017..3da56d8b 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -1,7 +1,9 @@  +using System; +using System.Linq; +using System.Reflection; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; -using System.Reflection; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.CrestronThread; using Crestron.SimplSharpPro.Diagnostics; @@ -9,12 +11,9 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using PepperDash.Essentials.Core.Web; -using System; -using System.Linq; -using Serilog.Events; using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Core.Web; +using Serilog.Events; namespace PepperDash.Essentials { @@ -46,22 +45,22 @@ namespace PepperDash.Essentials // AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; } - private System.Reflection.Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) + private Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) { - var assemblyName = new System.Reflection.AssemblyName(args.Name).Name; + var assemblyName = new AssemblyName(args.Name).Name; if (assemblyName == "PepperDash_Core") { - return System.Reflection.Assembly.LoadFrom("PepperDashCore.dll"); + return Assembly.LoadFrom("PepperDashCore.dll"); } if (assemblyName == "PepperDash_Essentials_Core") { - return System.Reflection.Assembly.LoadFrom("PepperDash.Essentials.Core.dll"); + return Assembly.LoadFrom("PepperDash.Essentials.Core.dll"); } if (assemblyName == "Essentials Devices Common") { - return System.Reflection.Assembly.LoadFrom("PepperDash.Essentials.Devices.Common.dll"); + return Assembly.LoadFrom("PepperDash.Essentials.Devices.Common.dll"); } return null; @@ -79,7 +78,7 @@ namespace PepperDash.Essentials if (preventInitializationComplete) { Debug.LogMessage(LogEventLevel.Debug, "******************* InitializeSystem() Entering **********************"); - + _startTimer = new CTimer(StartSystem, preventInitializationComplete, StartupTime); _initializeEvent = new CEvent(true, false); DeviceManager.AllDevicesRegistered += (o, a) => @@ -88,7 +87,7 @@ namespace PepperDash.Essentials }; _initializeEvent.Wait(30000); Debug.LogMessage(LogEventLevel.Debug, "******************* InitializeSystem() Exiting **********************"); - + SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; } else @@ -99,7 +98,7 @@ namespace PepperDash.Essentials private void StartSystem(object preventInitialization) { - Debug.SetErrorLogMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose); + Debug.SetErrorLogMinimumDebugLevel(LogEventLevel.Verbose); DeterminePlatform(); @@ -201,8 +200,8 @@ namespace PepperDash.Essentials { 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{0} on {1} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series"); @@ -210,8 +209,8 @@ namespace PepperDash.Essentials 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); + + 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; } @@ -220,7 +219,7 @@ namespace PepperDash.Essentials + 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; } @@ -228,7 +227,7 @@ namespace PepperDash.Essentials 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; } @@ -236,7 +235,7 @@ namespace PepperDash.Essentials else // Handles Linux OS (Virtual Control) { //Debug.SetDebugLevel(2); - Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on Virtual Control Server", Global.AssemblyVersion); + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on Virtual Control Server", Global.AssemblyVersion); // Set path to User/ filePathPrefix = directoryPrefix + dirSeparator + "User" + dirSeparator; @@ -246,7 +245,7 @@ namespace PepperDash.Essentials } catch (Exception e) { - Debug.LogMessage(e, "Unable to determine platform due to exception"); + Debug.LogMessage(e, "Unable to determine platform due to exception"); } } @@ -262,11 +261,11 @@ namespace PepperDash.Essentials PluginLoader.AddProgramAssemblies(); _ = new Core.DeviceFactory(); - _ = new Devices.Common.DeviceFactory(); - _ = new DeviceFactory(); + // _ = new Devices.Common.DeviceFactory(); + // _ = new DeviceFactory(); - _ = new ProcessorExtensionDeviceFactory(); - _ = new MobileControlFactory(); + // _ = new ProcessorExtensionDeviceFactory(); + // _ = new MobileControlFactory(); Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); @@ -313,7 +312,7 @@ namespace PepperDash.Essentials } - + /// /// Verifies filesystem is set up. IR, SGD, and programX folders @@ -333,46 +332,46 @@ namespace PepperDash.Essentials Directory.Create(irDir); var sgdDir = Global.FilePathPrefix + "sgd"; - if (!Directory.Exists(sgdDir)) - Directory.Create(sgdDir); + 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)) + if (!Directory.Exists(joinmapDir)) Directory.Create(joinmapDir); - return configExists; - } + return configExists; + } - /// - /// TearDown method - /// - public void TearDown() - { - Debug.LogMessage(LogEventLevel.Information, "Tearing down existing system"); - DeviceManager.DeactivateAll(); + /// + /// TearDown method + /// + public void TearDown() + { + Debug.LogMessage(LogEventLevel.Information, "Tearing down existing system"); + DeviceManager.DeactivateAll(); - TieLineCollection.Default.Clear(); + TieLineCollection.Default.Clear(); - foreach (var key in DeviceManager.GetDevices()) - DeviceManager.RemoveDevice(key); + foreach (var key in DeviceManager.GetDevices()) + DeviceManager.RemoveDevice(key); - Debug.LogMessage(LogEventLevel.Information, "Tear down COMPLETE"); - } + Debug.LogMessage(LogEventLevel.Information, "Tear down COMPLETE"); + } - /// - /// - /// - void Load() - { - LoadDevices(); - LoadRooms(); - LoadLogoServer(); + /// + /// + /// + void Load() + { + LoadDevices(); + LoadRooms(); + LoadLogoServer(); - DeviceManager.ActivateAll(); + DeviceManager.ActivateAll(); LoadTieLines(); @@ -381,8 +380,8 @@ namespace PepperDash.Essentials if (mobileControl == null) return; mobileControl.LinkSystemMonitorToAppServer();*/ - - } + + } /// /// LoadDevices method @@ -414,8 +413,8 @@ namespace PepperDash.Essentials { 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, @@ -430,14 +429,14 @@ namespace PepperDash.Essentials if (newDev == null) newDev = Core.DeviceFactory.GetDevice(devConf); - if (newDev != null) - DeviceManager.AddDevice(newDev); - else + 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(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); } } Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded."); @@ -475,27 +474,28 @@ namespace PepperDash.Essentials /// 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) + foreach (var roomConfig in ConfigReader.ConfigObject.Rooms) { try { var room = Core.DeviceFactory.GetDevice(roomConfig); - if(room == null) + 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) + } + catch (Exception ex) { Debug.LogMessage(ex, "Exception loading room {roomKey}:{roomType}", null, roomConfig.Key, roomConfig.Type); continue; @@ -564,7 +564,7 @@ namespace PepperDash.Essentials } catch { - Debug.LogMessage(LogEventLevel.Information, "Unable to find logo information in any room config"); + Debug.LogMessage(LogEventLevel.Information, "Unable to find logo information in any room config"); return false; } } diff --git a/src/PepperDash.Essentials/Factory/DeviceFactory.cs b/src/PepperDash.Essentials/Factory/DeviceFactory.cs index 7a2df7d1..092a67a2 100644 --- a/src/PepperDash.Essentials/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials/Factory/DeviceFactory.cs @@ -3,11 +3,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; -using System.Reflection; - using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; @@ -39,14 +38,33 @@ namespace PepperDash.Essentials try { var factory = (IDeviceFactory)Activator.CreateInstance(type); - factory.LoadTypeFactories(); + LoadDeviceFactories(factory); } catch (Exception e) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Unable to load type: '{exception}' DeviceFactory: {factoryName}", e, type.Name); + 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) + { + 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); + } + } } } From 43989b9588e013e15e0c36e2fcb586d23e4f17cb Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 09:12:09 -0500 Subject: [PATCH 02/12] chore: move interfaces to their own files --- .../Devices/FIND HOMES Interfaces.cs | 35 ------------------- .../Devices/IAttachVideoStatus.cs | 13 +++++++ .../Devices/IDisplayUsage.cs | 10 ++++++ .../Devices/IMakeModel.cs | 22 ++++++++++++ .../Devices/IOnline.cs | 13 +++++++ 5 files changed, 58 insertions(+), 35 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/Devices/FIND HOMES Interfaces.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/IAttachVideoStatus.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/IDisplayUsage.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/IMakeModel.cs create mode 100644 src/PepperDash.Essentials.Core/Devices/IOnline.cs diff --git a/src/PepperDash.Essentials.Core/Devices/FIND HOMES Interfaces.cs b/src/PepperDash.Essentials.Core/Devices/FIND HOMES Interfaces.cs deleted file mode 100644 index 44b36134..00000000 --- a/src/PepperDash.Essentials.Core/Devices/FIND HOMES Interfaces.cs +++ /dev/null @@ -1,35 +0,0 @@ -using PepperDash.Core; - - -namespace PepperDash.Essentials.Core -{ - /// - /// Defines the contract for IOnline - /// - public interface IOnline - { - BoolFeedback IsOnline { get; } - } - - /// - /// Defines the contract for IAttachVideoStatus - /// - public interface IAttachVideoStatus : IKeyed - { - // Extension methods will depend on this - } - - /// - /// For display classes that can provide usage data - /// - public interface IDisplayUsage - { - IntFeedback LampHours { get; } - } - - public interface IMakeModel : IKeyed - { - string DeviceMake { get; } - string DeviceModel { get; } - } -} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/IAttachVideoStatus.cs b/src/PepperDash.Essentials.Core/Devices/IAttachVideoStatus.cs new file mode 100644 index 00000000..4b9b36ca --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/IAttachVideoStatus.cs @@ -0,0 +1,13 @@ +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + /// + /// Defines the contract for IAttachVideoStatus + /// + public interface IAttachVideoStatus : IKeyed + { + // Extension methods will depend on this + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/IDisplayUsage.cs b/src/PepperDash.Essentials.Core/Devices/IDisplayUsage.cs new file mode 100644 index 00000000..0d74d819 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/IDisplayUsage.cs @@ -0,0 +1,10 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// For display classes that can provide usage data + /// + public interface IDisplayUsage + { + IntFeedback LampHours { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/IMakeModel.cs b/src/PepperDash.Essentials.Core/Devices/IMakeModel.cs new file mode 100644 index 00000000..494c9af3 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/IMakeModel.cs @@ -0,0 +1,22 @@ +using PepperDash.Core; + + +namespace PepperDash.Essentials.Core +{ + + /// + /// Defines the contract for device make and model information + /// + public interface IMakeModel : IKeyed + { + /// + /// Gets the make of the device + /// + string DeviceMake { get; } + + /// + /// Gets the model of the device + /// + string DeviceModel { get; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/IOnline.cs b/src/PepperDash.Essentials.Core/Devices/IOnline.cs new file mode 100644 index 00000000..8c86a480 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Devices/IOnline.cs @@ -0,0 +1,13 @@ +namespace PepperDash.Essentials.Core +{ + /// + /// Defines the contract for IOnline + /// + public interface IOnline + { + /// + /// Gets a value indicating whether the device is online. + /// + BoolFeedback IsOnline { get; } + } +} \ No newline at end of file From 31143f56df2fdcfb51dd1dfcb1aabea2f45d1016 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 09:13:42 -0500 Subject: [PATCH 03/12] fix: modify how exceptions are printed to reduce noise When an exception occurs during the connect method, only the exception message will be printed at the Error log level. The entire stack trace will be printed when at the Verbose level. fixes #1273 --- src/PepperDash.Core/Comm/GenericSshClient.cs | 311 +++++++++---------- 1 file changed, 154 insertions(+), 157 deletions(-) diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index 3ba9059e..13192ed5 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -146,31 +146,31 @@ namespace PepperDash.Core base(key) { StreamDebugging = new CommunicationStreamDebugging(key); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - Key = key; - Hostname = hostname; - Port = port; - Username = username; - Password = password; - AutoReconnectIntervalMs = 5000; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Key = key; + Hostname = hostname; + Port = port; + Username = username; + Password = password; + AutoReconnectIntervalMs = 5000; ReconnectTimer = new CTimer(o => - { + { if (ConnectEnabled) { Connect(); } - }, 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; + /// + /// 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 => { @@ -179,18 +179,18 @@ namespace PepperDash.Core 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"); + /// + /// 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(); } } @@ -223,10 +223,7 @@ namespace PepperDash.Core this.LogDebug("Attempting connect"); // Cancel reconnect if running. - if (ReconnectTimer != null) - { - ReconnectTimer.Stop(); - } + ReconnectTimer?.Stop(); // Cleanup the old client if it already exists if (Client != null) @@ -268,20 +265,26 @@ namespace PepperDash.Core if (ie is SocketException) { - this.LogException(ie, "CONNECTION failure: Cannot reach host"); + this.LogError("CONNECTION failure: Cannot reach host"); + this.LogVerbose(ie, "Exception details: "); } if (ie is System.Net.Sockets.SocketException socketException) { - this.LogException(ie, "Connection failure: Cannot reach {host} on {port}", + this.LogError("Connection failure: Cannot reach {host} on {port}", Hostname, Port); + this.LogVerbose(socketException, "SocketException details: "); } if (ie is SshAuthenticationException) { - this.LogException(ie, "Authentication failure for username {userName}", Username); + this.LogError("Authentication failure for username {userName}", Username); + this.LogVerbose(ie, "AuthenticationException details: "); } else - this.LogException(ie, "Error on connect"); + { + this.LogError("Error on connect: {error}", ie.Message); + this.LogVerbose(ie, "Exception details: "); + } DisconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); @@ -291,7 +294,7 @@ namespace PepperDash.Core ReconnectTimer.Reset(AutoReconnectIntervalMs); } } - catch(SshOperationTimeoutException ex) + catch (SshOperationTimeoutException ex) { this.LogWarning("Connection attempt timed out: {message}", ex.Message); @@ -306,7 +309,8 @@ namespace PepperDash.Core catch (Exception e) { var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; - this.LogException(e, "Unhandled exception on connect"); + this.LogError("Unhandled exception on connect: {error}", e.Message); + this.LogVerbose(e, "Exception details: "); DisconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); if (AutoReconnect) @@ -323,18 +327,18 @@ namespace PepperDash.Core } } - /// - /// Disconnect method - /// - public void Disconnect() - { - ConnectEnabled = false; - // Stop trying reconnects, if we are - if (ReconnectTimer != null) - { - ReconnectTimer.Stop(); - // ReconnectTimer = null; - } + /// + /// Disconnect method + /// + public void Disconnect() + { + ConnectEnabled = false; + // Stop trying reconnects, if we are + if (ReconnectTimer != null) + { + ReconnectTimer.Stop(); + // ReconnectTimer = null; + } KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); } @@ -360,7 +364,7 @@ namespace PepperDash.Core } catch (Exception ex) { - this.LogException(ex,"Exception in Kill Client"); + this.LogException(ex, "Exception in Kill Client"); } } @@ -368,7 +372,7 @@ namespace PepperDash.Core /// Kills the stream /// void KillStream() - { + { try { if (TheStream != null) @@ -384,59 +388,59 @@ namespace PepperDash.Core { 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) - { + /// + /// 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 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); - } + 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) - { + } + + 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) - { + /// + /// 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) @@ -465,61 +469,54 @@ namespace PepperDash.Core /// void OnConnectionChange() { - if (ConnectionChange != null) - ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); + ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this)); } #region IBasicCommunication Members - /// - /// Sends text to the server - /// - /// - /// - /// SendText method - /// - 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) + /// + /// Sends text to the server + /// + /// The text to send + public void SendText(string text) + { + try { - this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); + 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) - { + } + catch (Exception ex) + { this.LogException(ex, "Exception sending text: '{message}'", text); - } - } + } + } /// /// Sends Bytes to the server /// - /// - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) - { + /// The bytes to send + public void SendBytes(byte[] bytes) + { try { if (Client != null && TheStream != null && IsConnected) @@ -545,38 +542,38 @@ namespace PepperDash.Core catch (Exception ex) { this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes)); - } - } - #endregion + } + } + #endregion -} + } -//***************************************************************************************************** -//***************************************************************************************************** -/// -/// Represents a SshConnectionChangeEventArgs -/// -public class SshConnectionChangeEventArgs : EventArgs - { + //***************************************************************************************************** + //***************************************************************************************************** + /// + /// Represents a SshConnectionChangeEventArgs + /// + public class SshConnectionChangeEventArgs : EventArgs + { /// /// Connection State /// public bool IsConnected { get; private set; } - /// - /// Gets or sets the UIsConnected - /// - public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } } + /// + /// Gets or sets the UIsConnected + /// + public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } } - /// - /// Gets or sets the Client - /// - public GenericSshClient Client { get; private set; } + /// + /// Gets or sets the Client + /// + public GenericSshClient Client { get; private set; } - /// - /// Gets or sets the Status - /// - public ushort Status { get { return Client.UStatus; } } + /// + /// Gets or sets the Status + /// + public ushort Status { get { return Client.UStatus; } } /// /// S+ Constructor From f0af9f8d19bc467f6a49785f0bc251f7a31c44c0 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 09:14:45 -0500 Subject: [PATCH 04/12] fix: mark IHasMultipleDisplays and associated enum as obsolete Fixes #1219 --- .../Devices/SourceListItem.cs | 4 +++- src/PepperDash.Essentials.Core/Room/Interfaces.cs | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs b/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs index d4fac061..d171b3d9 100644 --- a/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs +++ b/src/PepperDash.Essentials.Core/Devices/SourceListItem.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Core; @@ -248,6 +249,7 @@ namespace PepperDash.Essentials.Core /// /// Defines the valid destination types for SourceListItems in a room /// + [Obsolete] public enum eSourceListItemDestinationTypes { /// diff --git a/src/PepperDash.Essentials.Core/Room/Interfaces.cs b/src/PepperDash.Essentials.Core/Room/Interfaces.cs index 00fa0b7c..bffb80a3 100644 --- a/src/PepperDash.Essentials.Core/Room/Interfaces.cs +++ b/src/PepperDash.Essentials.Core/Room/Interfaces.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; using PepperDash.Core; @@ -28,6 +25,7 @@ namespace PepperDash.Essentials.Core /// /// For rooms with multiple displays /// + [Obsolete("Will be removed in a future version")] public interface IHasMultipleDisplays { Dictionary Displays { get; } @@ -40,7 +38,7 @@ namespace PepperDash.Essentials.Core { void RunRouteAction(string routeKey, string sourceListKey); - void RunRouteAction(string routeKey, string sourceListKey, Action successCallback); + void RunRouteAction(string routeKey, string sourceListKey, Action successCallback); } /// @@ -50,7 +48,7 @@ namespace PepperDash.Essentials.Core { void RunDirectRoute(string sourceKey, string destinationKey, eRoutingSignalType type = eRoutingSignalType.AudioVideo); } - + /// /// Describes a room with matrix routing /// @@ -139,7 +137,7 @@ namespace PepperDash.Essentials.Core bool HasEnvironmentalControlDevices { get; } } - public interface IRoomOccupancy:IKeyed + public interface IRoomOccupancy : IKeyed { IOccupancyStatusProvider RoomOccupancy { get; } bool OccupancyStatusProviderIsRemote { get; } From dc7f99e176b6b3dda777cafbaed3fbfc7c8d819c Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 09:18:22 -0500 Subject: [PATCH 05/12] fix: mark FeedbackBase default constructor as obsolete There are situations now where feedbacks in the feedback collection can be used to update things on UIs. If the feedback doesn't have a key, it can't be used for this purpose. --- .../Feedbacks/BoolFeedback.cs | 1 + .../Feedbacks/FeedbackBase.cs | 101 ++++++++++-------- .../Feedbacks/SerialFeedback.cs | 3 +- .../Feedbacks/StringFeedback.cs | 1 + 4 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedback.cs index bc4cfde5..eb3abbb4 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/BoolFeedback.cs @@ -42,6 +42,7 @@ namespace PepperDash.Essentials.Core /// 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) { diff --git a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs index d4caa7e5..723d22a6 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/FeedbackBase.cs @@ -9,45 +9,54 @@ using PepperDash.Core; namespace PepperDash.Essentials.Core { - public abstract class Feedback : IKeyed - { - public event EventHandler OutputChange; + /// + /// Base class for all feedback types + /// + public abstract class Feedback : IKeyed + { + /// + /// Occurs when the output value changes + /// + public event EventHandler OutputChange; /// /// Gets or sets the Key /// public string Key { get; private set; } - /// - /// Gets or sets the BoolValue - /// - /// - public virtual bool BoolValue { get { return false; } } - /// - /// Gets or sets the IntValue - /// - public virtual int IntValue { get { return 0; } } - /// - /// Gets or sets the StringValue - /// - public virtual string StringValue { get { return ""; } } + /// + /// Gets or sets the BoolValue + /// + /// + public virtual bool BoolValue { get { return false; } } + /// + /// Gets or sets the IntValue + /// + public virtual int IntValue { get { return 0; } } + /// + /// Gets or sets the StringValue + /// + public virtual string StringValue { get { return ""; } } /// /// Gets or sets the SerialValue /// public virtual string SerialValue { get { return ""; } } - /// - /// Gets or sets the InTestMode - /// - public bool InTestMode { get; protected set; } + /// + /// Gets or sets the InTestMode + /// + public bool InTestMode { get; protected set; } - /// - /// Base Constructor - empty - /// - protected Feedback() - { - } + /// + /// 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 + /// + /// The key for the feedback protected Feedback(string key) { if (key == null) @@ -58,27 +67,27 @@ namespace PepperDash.Essentials.Core - /// - /// ClearTestValue method - /// - public void ClearTestValue() - { - InTestMode = false; - FireUpdate(); - } + /// + /// ClearTestValue method + /// + public void ClearTestValue() + { + InTestMode = false; + FireUpdate(); + } - /// - /// Fires an update synchronously - /// - public abstract void FireUpdate(); + /// + /// Fires an update synchronously + /// + public abstract void FireUpdate(); - /// - /// Fires the update asynchronously within a CrestronInvoke - /// - public void InvokeFireUpdate() - { - CrestronInvoke.BeginInvoke(o => FireUpdate()); - } + /// + /// Fires the update asynchronously within a CrestronInvoke + /// + public void InvokeFireUpdate() + { + CrestronInvoke.BeginInvoke(o => FireUpdate()); + } /// /// Helper method that fires event. Use this intstead of calling OutputChange @@ -103,5 +112,5 @@ 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/SerialFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs index 0daec8c1..b26e23e6 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/SerialFeedback.cs @@ -13,7 +13,7 @@ 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,6 +25,7 @@ namespace PepperDash.Essentials.Core List LinkedInputSigs = new List(); + [Obsolete("use constructor with Key parameter. This constructor will be removed in a future version")] public SerialFeedback() { } diff --git a/src/PepperDash.Essentials.Core/Feedbacks/StringFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/StringFeedback.cs index fe6d6884..f7d594c4 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/StringFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/StringFeedback.cs @@ -38,6 +38,7 @@ namespace PepperDash.Essentials.Core /// 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) { From 226014fee04f314e5c1fc8ec5d4fe86f673efeb5 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 09:27:33 -0500 Subject: [PATCH 06/12] fix: apply suggestions from code review - remove commented out debug statements - null check for description attribute Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs | 6 ++++-- src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs | 4 +++- .../MobileControlFactory.cs | 4 +++- src/PepperDash.Essentials/Factory/DeviceFactory.cs | 6 ++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index 8978c752..b77d279b 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -523,9 +523,11 @@ namespace PepperDash.Essentials { 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; + string description = descriptionAttribute != null && descriptionAttribute.Length > 0 + ? descriptionAttribute[0].Description + : "No description available"; var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } diff --git a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs index a0fa0c9f..ff730706 100644 --- a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs @@ -52,7 +52,9 @@ namespace PepperDash.Essentials.Devices.Common { //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; + string description = (descriptionAttribute != null && descriptionAttribute.Length > 0) + ? descriptionAttribute[0].Description + : "No description available"; 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.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index ce40aa64..32a00b8c 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -52,7 +52,9 @@ namespace PepperDash.Essentials { //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; + string description = (descriptionAttribute != null && descriptionAttribute.Length > 0) + ? descriptionAttribute[0].Description + : string.Empty; // Default value if no DescriptionAttribute is found 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/Factory/DeviceFactory.cs b/src/PepperDash.Essentials/Factory/DeviceFactory.cs index 092a67a2..e59628ab 100644 --- a/src/PepperDash.Essentials/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials/Factory/DeviceFactory.cs @@ -59,9 +59,11 @@ namespace PepperDash.Essentials { 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; + string description = (descriptionAttribute != null && descriptionAttribute.Length > 0) + ? descriptionAttribute[0].Description + : "No description available"; var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } From 08cc84a8e882ff2d186132e258f89b8e6b92c087 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 09:28:53 -0500 Subject: [PATCH 07/12] fix: apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Devices/ProcessorExtensionDeviceFactory.cs | 4 +++- src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs index 469d18ec..c8528c4a 100644 --- a/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs @@ -21,7 +21,9 @@ namespace PepperDash.Essentials.Core { //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; + string description = descriptionAttribute != null && descriptionAttribute.Length > 0 + ? descriptionAttribute[0].Description + : throw new InvalidOperationException($"No DescriptionAttribute found for type {typeof(T).FullName}"); var snippetAttribute = typeof(T).GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; ProcessorExtensionDeviceFactory.AddFactoryForType(typeName.ToLower(), description, typeof(T), BuildDevice); } diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs index 08aa67cf..9f644b04 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -92,9 +92,11 @@ namespace PepperDash.Essentials.Core { 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; + string description = descriptionAttribute != null && descriptionAttribute.Length > 0 + ? descriptionAttribute[0].Description + : "No description available"; var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } From 58bcc3315d70dfdac9cc06f4389d9aec3825c0dc Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 09:51:04 -0500 Subject: [PATCH 08/12] fix: add changes from code review --- Crestron-Library-Usage-Analysis.md | 282 ++++++++++++++++++ .../ProcessorExtensionDeviceFactory.cs | 7 +- .../Feedbacks/IntFeedback.cs | 1 + 3 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 Crestron-Library-Usage-Analysis.md diff --git a/Crestron-Library-Usage-Analysis.md b/Crestron-Library-Usage-Analysis.md new file mode 100644 index 00000000..e295d0db --- /dev/null +++ b/Crestron-Library-Usage-Analysis.md @@ -0,0 +1,282 @@ +# Crestron Library Usage Analysis - PepperDash Essentials + +This document provides a comprehensive analysis of Crestron classes and interfaces used throughout the PepperDash Essentials framework, organized by namespace and library component. + +## Executive Summary + +The PepperDash Essentials framework extensively leverages Crestron SDK components across 100+ files, providing abstractions for: +- Control system hardware (processors, touchpanels, IO devices) +- Communication interfaces (Serial, TCP/IP, SSH, CEC, IR) +- Device management and routing +- User interface components and smart objects +- System monitoring and diagnostics + +## 1. Core Crestron Libraries + +### 1.1 Crestron.SimplSharp + +**Primary Usage**: Foundational framework components, collections, and basic types. + +**Key Files**: +- Multiple files across all projects use `Crestron.SimplSharp` namespaces +- Provides basic C# runtime support for Crestron processors + +### 1.2 Crestron.SimplSharpPro + +**Primary Usage**: Main hardware abstraction layer for Crestron devices. + +**Key Classes Used**: + +#### CrestronControlSystem +- **File**: `/src/PepperDash.Essentials/ControlSystem.cs` +- **Usage**: Base class for the main control system implementation +- **Implementation**: `public class ControlSystem : CrestronControlSystem, ILoadConfig` + +#### Device (Base Class) +- **Files**: 50+ files inherit from or use this class +- **Key Implementations**: + - `/src/PepperDash.Core/Device.cs` - Core device abstraction + - `/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs` - Extended device base + - `/src/PepperDash.Essentials.Core/Room/Room.cs` - Room device implementation + - `/src/PepperDash.Essentials.Core/Devices/CrestronProcessor.cs` - Processor device wrapper + +#### BasicTriList +- **Files**: 30+ files use this class extensively +- **Primary Usage**: Touchpanel communication and SIMPL bridging +- **Key Files**: + - `/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs` - Extension methods for signal handling + - `/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs` - Bridge interface + - `/src/PepperDash.Essentials.Core/Touchpanels/ModalDialog.cs` - UI dialog implementation + +#### BasicTriListWithSmartObject +- **Files**: Multiple touchpanel and UI files +- **Usage**: Enhanced touchpanel support with smart object integration +- **Key Files**: + - `/src/PepperDash.Essentials.Core/Touchpanels/Interfaces.cs` - Interface definitions + - `/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferenceList/SubpageReferenceList.cs` + +## 2. Communication Hardware + +### 2.1 Serial Communication (ComPort) + +**Primary Class**: `ComPort` +**Key Files**: +- `/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs` +- `/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs` + +**Usage Pattern**: +```csharp +public class ComPortController : Device, IBasicCommunicationWithStreamDebugging +public static ComPort GetComPort(EssentialsControlPropertiesConfig config) +``` + +**Interface Support**: `IComPorts` - Used for devices that provide multiple COM ports + +### 2.2 IR Communication (IROutputPort) + +**Primary Class**: `IROutputPort` +**Key Files**: +- `/src/PepperDash.Essentials.Core/Devices/IrOutputPortController.cs` +- `/src/PepperDash.Essentials.Core/Devices/GenericIRController.cs` +- `/src/PepperDash.Essentials.Core/Comm and IR/IRPortHelper.cs` + +**Usage Pattern**: +```csharp +public class IrOutputPortController : Device +IROutputPort IrPort; +public IrOutputPortController(string key, IROutputPort port, string irDriverFilepath) +``` + +### 2.3 CEC Communication (ICec) + +**Primary Interface**: `ICec` +**Key Files**: +- `/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs` +- `/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs` + +**Usage Pattern**: +```csharp +public class CecPortController : Device, IBasicCommunicationWithStreamDebugging +public static ICec GetCecPort(ControlPropertiesConfig config) +``` + +## 3. Input/Output Hardware + +### 3.1 Digital Input + +**Primary Interface**: `IDigitalInput` +**Key Files**: +- `/src/PepperDash.Essentials.Core/CrestronIO/GenericDigitalInputDevice.cs` +- `/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs` + +**Usage Pattern**: +```csharp +public List Inputs { get; private set; } +void AddInput(IDigitalInput input) +``` + +### 3.2 Versiport Support + +**Key Files**: +- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportInputDevice.cs` +- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportAnalogInputDevice.cs` +- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportOutputDevice.cs` + +**Usage**: Provides flexible I/O port configuration for various signal types + +## 4. Touchpanel Hardware + +### 4.1 MPC3 Touchpanel + +**Primary Class**: `MPC3Basic` +**Key File**: `/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs` + +**Usage Pattern**: +```csharp +public class Mpc3TouchpanelController : Device +readonly MPC3Basic _touchpanel; +_touchpanel = processor.ControllerTouchScreenSlotDevice as MPC3Basic; +``` + +### 4.2 TSW Series Support + +**Evidence**: References found in messenger files and mobile control components +**Usage**: Integrated through mobile control messaging system for TSW touchpanel features + +## 5. Timer and Threading + +### 5.1 CTimer + +**Primary Class**: `CTimer` +**Key File**: `/src/PepperDash.Core/PasswordManagement/PasswordManager.cs` + +**Usage Pattern**: +```csharp +Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Started")); +Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Reset")); +``` + +## 6. Networking and Communication + +### 6.1 Ethernet Communication + +**Libraries Used**: +- `Crestron.SimplSharpPro.EthernetCommunication` +- `Crestron.SimplSharp.Net.Utilities.EthernetHelper` + +**Key Files**: +- `/src/PepperDash.Core/Comm/GenericTcpIpClient.cs` +- `/src/PepperDash.Core/Comm/GenericTcpIpServer.cs` +- `/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs` +- `/src/PepperDash.Core/Comm/GenericSshClient.cs` +- `/src/PepperDash.Core/Comm/GenericUdpServer.cs` + +**Usage Pattern**: +```csharp +public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect +public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect +``` + +## 7. Device Management Libraries + +### 7.1 DeviceSupport + +**Library**: `Crestron.SimplSharpPro.DeviceSupport` +**Usage**: Core device support infrastructure used throughout the framework + +### 7.2 DM (DigitalMedia) + +**Library**: `Crestron.SimplSharpPro.DM` +**Usage**: Digital media routing and switching support +**Evidence**: Found in routing configuration and DM output card references + +## 8. User Interface Libraries + +### 8.1 UI Components + +**Library**: `Crestron.SimplSharpPro.UI` +**Usage**: User interface elements and touchpanel controls + +### 8.2 Smart Objects + +**Key Files**: +- `/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDynamicList.cs` +- `/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferenceList/SubpageReferenceList.cs` + +**Usage**: Advanced UI components with dynamic content + +## 9. System Monitoring and Diagnostics + +### 9.1 Diagnostics + +**Library**: `Crestron.SimplSharpPro.Diagnostics` +**Usage**: System health monitoring and performance tracking + +### 9.2 System Information + +**Key Files**: +- `/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs` + +**Usage**: Provides system status, Ethernet information, and program details + +## 10. Integration Patterns + +### 10.1 SIMPL Bridging + +**Pattern**: Extensive use of `BasicTriList` for SIMPL integration +**Files**: Bridge classes throughout the framework implement `LinkToApi` methods: +```csharp +public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge); +``` + +### 10.2 Device Factory Pattern + +**Implementation**: Factory classes create hardware-specific implementations +**Example**: `CommFactory.cs` provides communication device creation + +### 10.3 Extension Methods + +**Pattern**: Extensive use of extension methods for Crestron classes +**Example**: `TriListExtensions.cs` adds 30+ extension methods to `BasicTriList` + +## 11. Signal Processing + +### 11.1 Signal Types + +**Bool Signals**: Digital control and feedback +**UShort Signals**: Analog values and numeric data +**String Signals**: Text and configuration data + +**Implementation**: Comprehensive signal handling in `TriListExtensions.cs` + +## 12. Error Handling and Logging + +**Pattern**: Consistent use of Crestron's Debug logging throughout +**Examples**: +```csharp +Debug.LogMessage(LogEventLevel.Information, "Device {0} is not a valid device", dc.PortDeviceKey); +Debug.LogMessage(LogEventLevel.Debug, "Error Waking Panel. Maybe testing with Xpanel?"); +``` + +## 13. Threading and Synchronization + +**Components**: +- CTimer for time-based operations +- Thread-safe collections and patterns +- Event-driven programming models + +## Conclusion + +The PepperDash Essentials framework demonstrates sophisticated integration with the Crestron ecosystem, leveraging: + +- **Core Infrastructure**: CrestronControlSystem, Device base classes +- **Communication**: COM, IR, CEC, TCP/IP, SSH protocols +- **Hardware Abstraction**: Touchpanels, I/O devices, processors +- **User Interface**: Smart objects, signal processing, SIMPL bridging +- **System Services**: Monitoring, diagnostics, device management + +This analysis shows that Essentials serves as a comprehensive middleware layer, abstracting Crestron hardware complexities while providing modern software development patterns and practices. + +--- +*Generated: [Current Date]* +*Framework Version: PepperDash Essentials (Based on codebase analysis)* diff --git a/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs index c8528c4a..514e9a20 100644 --- a/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs @@ -19,11 +19,10 @@ namespace PepperDash.Essentials.Core { 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 != null && descriptionAttribute.Length > 0 - ? descriptionAttribute[0].Description - : throw new InvalidOperationException($"No DescriptionAttribute found for type {typeof(T).FullName}"); + string description = descriptionAttribute != null && descriptionAttribute.Length > 0 + ? descriptionAttribute[0].Description + : "No description available"; var snippetAttribute = typeof(T).GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; ProcessorExtensionDeviceFactory.AddFactoryForType(typeName.ToLower(), description, typeof(T), BuildDevice); } diff --git a/src/PepperDash.Essentials.Core/Feedbacks/IntFeedback.cs b/src/PepperDash.Essentials.Core/Feedbacks/IntFeedback.cs index 06e35850..ee23e310 100644 --- a/src/PepperDash.Essentials.Core/Feedbacks/IntFeedback.cs +++ b/src/PepperDash.Essentials.Core/Feedbacks/IntFeedback.cs @@ -43,6 +43,7 @@ namespace PepperDash.Essentials.Core /// 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) { From 4fc6ecbd0b57c4b102523406f9c2b6af4b127697 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 09:56:39 -0500 Subject: [PATCH 09/12] style: switch to auto property for attributes --- .../Devices/ConfigSnippetAttribute.cs | 10 ++-------- .../Devices/DescriptionAttribute.cs | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs b/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs index 76d3640c..a1fdef35 100644 --- a/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs +++ b/src/PepperDash.Essentials.Core/Devices/ConfigSnippetAttribute.cs @@ -8,26 +8,20 @@ namespace PepperDash.Essentials.Core [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] public class ConfigSnippetAttribute : Attribute { - private string _ConfigSnippet; - /// /// Represents a configuration snippet for the device. /// /// public ConfigSnippetAttribute(string configSnippet) { - //Debug.LogMessage(LogEventLevel.Verbose, "Setting Config Snippet {0}", configSnippet); - _ConfigSnippet = 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 { return _ConfigSnippet; } - } + 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 index 6d4073d9..abe51665 100644 --- a/src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs +++ b/src/PepperDash.Essentials.Core/Devices/DescriptionAttribute.cs @@ -8,25 +8,19 @@ namespace PepperDash.Essentials.Core [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] public class DescriptionAttribute : Attribute { - private string _Description; - /// /// Represents a description attribute for a device. /// /// public DescriptionAttribute(string description) { - //Debug.LogMessage(LogEventLevel.Verbose, "Setting Description: {0}", description); - _Description = description; + Description = description; } /// /// Gets the description for the device. /// - public string Description - { - get { return _Description; } - } + public string Description { get; } } } \ No newline at end of file From ee6f9416a3bc976165469c7bdd12d41855f2449f Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 10:24:32 -0500 Subject: [PATCH 10/12] chore: remove unused configSnippet --- .../ProcessorExtensionDeviceFactory.cs | 11 ++-- .../Factory/DeviceFactory.cs | 8 ++- .../Plugins/PluginLoader.cs | 54 ++----------------- .../DeviceFactory.cs | 7 ++- .../MobileControlFactory.cs | 12 ++--- .../Factory/DeviceFactory.cs | 16 ++---- 6 files changed, 26 insertions(+), 82 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs index 514e9a20..8549fd38 100644 --- a/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Devices/ProcessorExtensionDeviceFactory.cs @@ -1,8 +1,14 @@ +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 @@ -19,11 +25,10 @@ namespace PepperDash.Essentials.Core { foreach (var typeName in TypeNames) { - var descriptionAttribute = typeof(T).GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; - string description = descriptionAttribute != null && descriptionAttribute.Length > 0 + string description = typeof(T).GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0 ? descriptionAttribute[0].Description : "No description available"; - var snippetAttribute = typeof(T).GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + ProcessorExtensionDeviceFactory.AddFactoryForType(typeName.ToLower(), description, typeof(T), BuildDevice); } } diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs index 9f644b04..b4852f92 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -92,12 +92,10 @@ namespace PepperDash.Essentials.Core { foreach (var typeName in deviceFactory.TypeNames) { - - var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; - string description = descriptionAttribute != null && descriptionAttribute.Length > 0 - ? descriptionAttribute[0].Description + string description = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0 + ? descriptionAttribute[0].Description : "No description available"; - var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } diff --git a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs index b77d279b..a1dd9413 100644 --- a/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs +++ b/src/PepperDash.Essentials.Core/Plugins/PluginLoader.cs @@ -523,62 +523,14 @@ namespace PepperDash.Essentials { foreach (var typeName in deviceFactory.TypeNames) { - - var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; - string description = descriptionAttribute != null && descriptionAttribute.Length > 0 - ? descriptionAttribute[0].Description + string description = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0 + ? descriptionAttribute[0].Description : "No description available"; - var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } - /// - /// Loads a a custom plugin via the legacy method - /// - /// - /// - static void LoadCustomLegacyPlugin(Type type, MethodInfo loadPlugin, LoadedAssembly loadedAssembly) - { - Debug.LogMessage(LogEventLevel.Verbose, "LoadPlugin method found in {0}", type.Name); - - var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static); - - var minimumVersion = fields.FirstOrDefault(p => p.Name.Equals("MinimumEssentialsFrameworkVersion")); - if (minimumVersion != null) - { - Debug.LogMessage(LogEventLevel.Verbose, "MinimumEssentialsFrameworkVersion found"); - - var minimumVersionString = minimumVersion.GetValue(null) as string; - - if (!string.IsNullOrEmpty(minimumVersionString)) - { - var passed = Global.IsRunningMinimumVersionOrHigher(minimumVersionString); - - if (!passed) - { - Debug.LogMessage(LogEventLevel.Information, "Plugin indicates minimum Essentials version {0}. Dependency check failed. Skipping Plugin", minimumVersionString); - return; - } - else - { - Debug.LogMessage(LogEventLevel.Information, "Passed plugin passed dependency check (required version {0})", minimumVersionString); - } - } - else - { - Debug.LogMessage(LogEventLevel.Information, "MinimumEssentialsFrameworkVersion found but not set. Loading plugin, but your mileage may vary."); - } - } - else - { - Debug.LogMessage(LogEventLevel.Information, "MinimumEssentialsFrameworkVersion not found. Loading plugin, but your mileage may vary."); - } - - Debug.LogMessage(LogEventLevel.Information, "Loading legacy plugin: {0}", loadedAssembly.Name); - loadPlugin.Invoke(null, null); - - } /// /// LoadPlugins method diff --git a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs index ff730706..178c53a7 100644 --- a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs @@ -51,11 +51,10 @@ namespace PepperDash.Essentials.Devices.Common 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 != null && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description + string description = (deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0) + ? descriptionAttribute[0].Description : "No description available"; - 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.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index 32a00b8c..393b3385 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -50,13 +50,11 @@ namespace PepperDash.Essentials { 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 != null && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description - : string.Empty; // Default value if no DescriptionAttribute is found - var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; - Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); + 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 + + DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } } diff --git a/src/PepperDash.Essentials/Factory/DeviceFactory.cs b/src/PepperDash.Essentials/Factory/DeviceFactory.cs index e59628ab..e60b3fa4 100644 --- a/src/PepperDash.Essentials/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials/Factory/DeviceFactory.cs @@ -1,17 +1,11 @@  using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronIO; -using Crestron.SimplSharpPro; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; + using PepperDash.Core; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.Config; namespace PepperDash.Essentials { @@ -59,12 +53,10 @@ namespace PepperDash.Essentials { foreach (var typeName in deviceFactory.TypeNames) { - - var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; - string description = (descriptionAttribute != null && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description + string description = (deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0) + ? descriptionAttribute[0].Description : "No description available"; - var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } From 615f640ebbebbf7dcc43948b9f6b4554c9023e61 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 10:27:50 -0500 Subject: [PATCH 11/12] fix: use continue instead of return Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 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 b4852f92..06e685d3 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -155,7 +155,7 @@ namespace PepperDash.Essentials.Core prop.Parent.Replace(secret); } - if (!(prop.Value is JObject recurseProp)) return; + if (!(prop.Value is JObject recurseProp)) continue; CheckForSecrets(recurseProp.Properties()); } From efe70208d348a5766f8b1c26785fd25ad732877c Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 25 Jul 2025 10:32:35 -0500 Subject: [PATCH 12/12] fix: check for null assembly name --- src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs index 06e685d3..b723f6c9 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -53,9 +53,9 @@ namespace PepperDash.Essentials.Core // 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); + Debug.LogDebug("loaded assembly: {assemblyName}", assembly.GetName()?.Name ?? "Unknown"); - PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly); + PluginLoader.AddLoadedAssembly(assembly.GetName()?.Name ?? "Unknown", assembly); var types = assembly.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract);