diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index c199773a..915b10d0 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -8,527 +8,528 @@ using PepperDash.Core; using Serilog.Events; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + +public static class DeviceManager { + public static event EventHandler AllDevicesActivated; + public static event EventHandler AllDevicesRegistered; + public static event EventHandler AllDevicesInitialized; + + private static readonly CCriticalSection DeviceCriticalSection = new CCriticalSection(); + + //public static List Devices { get { return _Devices; } } + //static List _Devices = new List(); + + private static readonly Dictionary Devices = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// - /// Manages the devices in the system + /// Returns a copy of all the devices in a list /// - public static class DeviceManager + public static List AllDevices => [.. Devices.Values]; + + public static bool AddDeviceEnabled; + + /// + /// Initializes the control system by enabling device management and registering console commands. + /// + /// This method sets up the control system for device management by enabling the addition of + /// devices and registering a series of console commands for interacting with devices. These commands allow + /// operators to list device statuses, feedbacks, and managed devices, as well as perform actions such as + /// simulating communication, debugging device streams, and accessing device properties and methods. + /// The instance representing the control system to initialize. + public static void Initialize(CrestronControlSystem cs) { - /// - /// Raised when all devices have been activated - /// - public static event EventHandler AllDevicesActivated; + AddDeviceEnabled = true; - /// - /// Raised when all devices have been registered - /// - public static event EventHandler AllDevicesRegistered; + CrestronConsole.AddNewConsoleCommand(ListDeviceCommStatuses, "devcommstatus", "Lists the communication status of all devices", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListDeviceFeedbacks, "devfb", "Lists current feedbacks", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListDevices, "devlist", "Lists current managed devices", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(DeviceJsonApi.DoDeviceActionWithJson, "devjson", "", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetProperties(s)), "devprops", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetMethods(s)), "devmethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetApiMethods(s)), "apimethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(SimulateComReceiveOnDevice, "devsimreceive", + "Simulates incoming data on a com device", ConsoleAccessLevelEnum.AccessOperator); - /// - /// Raised when all devices have been initialized - /// - public static event EventHandler AllDevicesInitialized; + CrestronConsole.AddNewConsoleCommand(SetDeviceStreamDebugging, "setdevicestreamdebug", "set comm debug [deviceKey] [off/rx/tx/both] ([minutes])", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => DisableAllDeviceStreamDebugging(), "disableallstreamdebug", "disables stream debugging on all devices", ConsoleAccessLevelEnum.AccessOperator); + } - private static readonly CCriticalSection DeviceCriticalSection = new CCriticalSection(); - - private static readonly CEvent AllowAddDevicesCEvent = new CEvent(false, true); - - private static readonly Dictionary Devices = new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets or sets the AllDevices - /// - public static List AllDevices { get { return new List(Devices.Values); } } - - /// - /// Gets or sets the AddDeviceEnabled - /// - public static bool AddDeviceEnabled; - - /// - /// Initialize method - /// - public static void Initialize(CrestronControlSystem cs) + /// + /// Calls activate steps on all Device class items + /// + public static void ActivateAll() + { + try { - AddDeviceEnabled = true; - CrestronConsole.AddNewConsoleCommand(ListDeviceCommStatuses, "devcommstatus", "Lists the communication status of all devices", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(ListDeviceFeedbacks, "devfb", "Lists current feedbacks", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(ListDevices, "devlist", "Lists current managed devices", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(DeviceJsonApi.DoDeviceActionWithJson, "devjson", "", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetProperties(s).Replace(Environment.NewLine, "\r\n")), "devprops", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetMethods(s).Replace(Environment.NewLine, "\r\n")), "devmethods", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetApiMethods(s).Replace(Environment.NewLine, "\r\n")), "apimethods", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(SimulateComReceiveOnDevice, "devsimreceive", - "Simulates incoming data on a com device", ConsoleAccessLevelEnum.AccessOperator); + OnAllDevicesRegistered(); - CrestronConsole.AddNewConsoleCommand(SetDeviceStreamDebugging, "setdevicestreamdebug", "set comm debug [deviceKey] [off/rx/tx/both] ([minutes])", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => DisableAllDeviceStreamDebugging(), "disableallstreamdebug", "disables stream debugging on all devices", ConsoleAccessLevelEnum.AccessOperator); - } - - /// - /// ActivateAll method - /// - public static void ActivateAll() - { - try - { - OnAllDevicesRegistered(); - - DeviceCriticalSection.Enter(); - AddDeviceEnabled = false; - // PreActivate all devices - Debug.LogMessage(LogEventLevel.Information, "****PreActivation starting...****"); - foreach (var d in Devices.Values) - { - try - { - if (d is Device) - (d as Device).PreActivate(); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} PreActivation failure: {0}", e.Message, d.Key); - Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); - } - } - Debug.LogMessage(LogEventLevel.Information, "****PreActivation complete****"); - Debug.LogMessage(LogEventLevel.Information, "****Activation starting...****"); - - // Activate all devices - foreach (var d in Devices.Values) - { - try - { - if (d is Device) - (d as Device).Activate(); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} Activation failure: {0}", e.Message, d.Key); - Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); - } - } - - Debug.LogMessage(LogEventLevel.Information, "****Activation complete****"); - Debug.LogMessage(LogEventLevel.Information, "****PostActivation starting...****"); - - // PostActivate all devices - foreach (var d in Devices.Values) - { - try - { - if (d is Device) - (d as Device).PostActivate(); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} PostActivation failure: {0}", e.Message, d.Key); - Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); - } - } - - Debug.LogMessage(LogEventLevel.Information, "****PostActivation complete****"); - - OnAllDevicesActivated(); - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - private static void DeviceManager_Initialized(object sender, EventArgs e) - { - var allInitialized = Devices.Values.OfType().All(d => d.IsInitialized); - - if (allInitialized) - { - Debug.LogMessage(LogEventLevel.Information, "****All Devices Initalized****"); - - OnAllDevicesInitialized(); - } - } - - private static void OnAllDevicesActivated() - { - var handler = AllDevicesActivated; - if (handler != null) - { - handler(null, new EventArgs()); - } - } - - private static void OnAllDevicesRegistered() - { - var handler = AllDevicesRegistered; - if (handler != null) - { - handler(null, new EventArgs()); - } - } - - private static void OnAllDevicesInitialized() - { - var handler = AllDevicesInitialized; - if (handler != null) - { - handler(null, new EventArgs()); - } - } - - /// - /// DeactivateAll method - /// - public static void DeactivateAll() - { - try - { - DeviceCriticalSection.Enter(); - foreach (var d in Devices.Values.OfType()) - { - d.Deactivate(); - } - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - private static void ListDevices(string s) - { - CrestronConsole.ConsoleCommandResponse($"{Devices.Count} Devices registered with Device Manager:\r\n"); - - var sorted = Devices.Values.ToList(); - sorted.Sort((a, b) => a.Key.CompareTo(b.Key)); - - foreach (var d in sorted) - { - var name = d is IKeyName ? (d as IKeyName).Name : "---"; - CrestronConsole.ConsoleCommandResponse($" [{d.Key}] {name}\r\n"); - } - } - - private static void ListDeviceFeedbacks(string devKey) - { - var dev = GetDeviceForKey(devKey); - if (dev == null) - { - CrestronConsole.ConsoleCommandResponse($"Device '{devKey}' not found\r\n"); - return; - } - if (!(dev is IHasFeedback statusDev)) - { - CrestronConsole.ConsoleCommandResponse($"Device '{devKey}' does not have visible feedbacks\r\n"); - return; - } - statusDev.DumpFeedbacksToConsole(true); - } - - private static void ListDeviceCommStatuses(string input) - { - - foreach (var dev in Devices.Values.OfType()) - { - CrestronConsole.ConsoleCommandResponse($"{dev}: {dev.CommunicationMonitor.Status}\r\n"); - } - } - - /// - /// AddDevice method - /// - public static void AddDevice(IKeyed newDev) - { - try - { - if (!DeviceCriticalSection.TryEnter()) - { - Debug.LogMessage(LogEventLevel.Information, "Currently unable to add devices to Device Manager. Please try again"); - return; - } - // Check for device with same key - //var existingDevice = _Devices.FirstOrDefault(d => d.Key.Equals(newDev.Key, StringComparison.OrdinalIgnoreCase)); - ////// If it exists, remove or warn?? - //if (existingDevice != null) - - if (!AddDeviceEnabled) - { - Debug.LogMessage(LogEventLevel.Information, "All devices have been activated. Adding new devices is not allowed."); - return; - } - - if (Devices.ContainsKey(newDev.Key)) - { - Debug.LogMessage(LogEventLevel.Information, newDev, "WARNING: A device with this key already exists. Not added to manager"); - return; - } - Devices.Add(newDev.Key, newDev); - //if (!(_Devices.Contains(newDev))) - // _Devices.Add(newDev); - - if (newDev is EssentialsDevice essentialsDev) - essentialsDev.Initialized += DeviceManager_Initialized; - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - /// - /// AddDevice method - /// - public static void AddDevice(IEnumerable devicesToAdd) - { - try - { - if (!DeviceCriticalSection.TryEnter()) - { - Debug.LogMessage(LogEventLevel.Information, - "Currently unable to add devices to Device Manager. Please try again"); - return; - } - if (!AddDeviceEnabled) - { - Debug.LogMessage(LogEventLevel.Information, - "All devices have been activated. Adding new devices is not allowed."); - return; - } - - foreach (var dev in devicesToAdd) - { - try - { - Devices.Add(dev.Key, dev); - } - catch (ArgumentException ex) - { - Debug.LogMessage(LogEventLevel.Information, "Error adding device with key {0} to Device Manager: {1}\r\nStack Trace: {2}", - dev.Key, ex.Message, ex.StackTrace); - } - } - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - /// - /// RemoveDevice method - /// - public static void RemoveDevice(IKeyed newDev) - { - try - { - DeviceCriticalSection.Enter(); - if (newDev == null) - return; - if (Devices.ContainsKey(newDev.Key)) - Devices.Remove(newDev.Key); - //if (_Devices.Contains(newDev)) - // _Devices.Remove(newDev); - else - Debug.LogMessage(LogEventLevel.Information, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key); - } - finally - { - DeviceCriticalSection.Leave(); - } - } - - /// - /// GetDeviceKeys method - /// - public static IEnumerable GetDeviceKeys() - { - //return _Devices.Select(d => d.Key).ToList(); - return Devices.Keys; - } - - /// - /// GetDevices method - /// - public static IEnumerable GetDevices() - { - //return _Devices.Select(d => d.Key).ToList(); - return Devices.Values; - } - - /// - /// GetDeviceForKey method - /// - public static IKeyed GetDeviceForKey(string key) - { - //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (key != null && Devices.ContainsKey(key)) - return Devices[key]; - - return null; - } - - /// - /// GetDeviceForKey method - /// - /// - public static T GetDeviceForKey(string key) - { - //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (key == null || !Devices.ContainsKey(key)) - return default; - - if (!(Devices[key] is T)) - { - Debug.LogMessage(LogEventLevel.Error, "Device with key '{0}' is not of type '{1}'", key, typeof(T).Name); - return default; - } - - return (T)Devices[key]; - } - - /// - /// Console handler that simulates com port data receive - /// - /// - /// - /// SimulateComReceiveOnDevice method - /// - public static void SimulateComReceiveOnDevice(string s) - { - // devcomsim:1 xyzabc - var match = Regex.Match(s, @"(\S*)\s*(.*)"); - if (match.Groups.Count < 3) - { - CrestronConsole.ConsoleCommandResponse(" Format: devsimreceive:P "); - return; - } - //Debug.LogMessage(LogEventLevel.Verbose, "**** {0} - {1} ****", match.Groups[1].Value, match.Groups[2].Value); - - if (!(GetDeviceForKey(match.Groups[1].Value) is ComPortController com)) - { - CrestronConsole.ConsoleCommandResponse("'{0}' is not a comm port device", match.Groups[1].Value); - return; - } - com.SimulateReceive(match.Groups[2].Value); - } - - /// - /// Prints a list of routing inputs and outputs by device key. - /// - /// Device key from which to report data - /// - /// GetRoutingPorts method - /// - public static void GetRoutingPorts(string s) - { - var device = GetDeviceForKey(s); - - if (device == null) return; - var inputPorts = ((device as IRoutingInputs) != null) ? (device as IRoutingInputs).InputPorts : null; - var outputPorts = ((device as IRoutingOutputs) != null) ? (device as IRoutingOutputs).OutputPorts : null; - if (inputPorts != null) - { - CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine); - foreach (var routingInputPort in inputPorts) - { - CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine); - } - } - if (outputPorts == null) return; - CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine); - foreach (var routingOutputPort in outputPorts) - { - CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine); - } - } - - /// - /// Attempts to set the debug level of a device - /// - /// - /// - /// SetDeviceStreamDebugging method - /// - public static void SetDeviceStreamDebugging(string s) - { - if (String.IsNullOrEmpty(s) || s.Contains("?")) - { - CrestronConsole.ConsoleCommandResponse( - "SETDEVICESTREAMDEBUG [{deviceKey}] [OFF |TX | RX | BOTH] [timeOutInMinutes]\r\n" + - " {deviceKey} [OFF | TX | RX | BOTH] - Device to set stream debugging on, and which setting to use\r\n" + - " timeOutInMinutes - Set timeout for stream debugging. Default is 30 minutes"); - return; - } - - var args = s.Split(' '); - - var deviceKey = args[0]; - var setting = args[1]; - - var timeout = String.Empty; - - if (args.Length >= 3) - { - timeout = args[2]; - } - - - if (!(GetDeviceForKey(deviceKey) is IStreamDebugging device)) - { - CrestronConsole.ConsoleCommandResponse("Unable to get device with key: {0}", deviceKey); - return; - } - - eStreamDebuggingSetting debugSetting; - - try - { - debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), setting, true); - } - catch - { - CrestronConsole.ConsoleCommandResponse("Unable to convert setting value. Please use off/rx/tx/both"); - return; - } - - if (!string.IsNullOrEmpty(timeout)) + DeviceCriticalSection.Enter(); + AddDeviceEnabled = false; + // PreActivate all devices + Debug.LogMessage(LogEventLevel.Information, "****PreActivation starting...****"); + foreach (var d in Devices.Values) { try { - var min = Convert.ToUInt32(timeout); - - device.StreamDebugging.SetDebuggingWithSpecificTimeout(debugSetting, min); - CrestronConsole.ConsoleCommandResponse("Device: '{0}' debug level set to {1} for {2} minutes", deviceKey, debugSetting, min); - + if (d is Device) + (d as Device).PreActivate(); } catch (Exception e) { - CrestronConsole.ConsoleCommandResponse("Unable to convert minutes or settings value. Please use an integer value for minutes. Error: {0}", e); + Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} PreActivation failure: {0}", e.Message, d.Key); + Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); } } - else + Debug.LogMessage(LogEventLevel.Information, "****PreActivation complete****"); + Debug.LogMessage(LogEventLevel.Information, "****Activation starting...****"); + + // Activate all devices + foreach (var d in Devices.Values) { - device.StreamDebugging.SetDebuggingWithDefaultTimeout(debugSetting); - CrestronConsole.ConsoleCommandResponse("Device: '{0}' debug level set to {1} for default time (30 minutes)", deviceKey, debugSetting); + try + { + if (d is Device) + (d as Device).Activate(); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} Activation failure: {0}", e.Message, d.Key); + Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); + } + } + + Debug.LogMessage(LogEventLevel.Information, "****Activation complete****"); + Debug.LogMessage(LogEventLevel.Information, "****PostActivation starting...****"); + + // PostActivate all devices + foreach (var d in Devices.Values) + { + try + { + if (d is Device) + (d as Device).PostActivate(); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, d, "ERROR: Device {1} PostActivation failure: {0}", e.Message, d.Key); + Debug.LogMessage(LogEventLevel.Debug, d, "Stack Trace: {0}", e.StackTrace); + } + } + + Debug.LogMessage(LogEventLevel.Information, "****PostActivation complete****"); + + OnAllDevicesActivated(); + } + finally + { + DeviceCriticalSection.Leave(); + } + } + + private static void DeviceManager_Initialized(object sender, EventArgs e) + { + var allInitialized = Devices.Values.OfType().All(d => d.IsInitialized); + + if (allInitialized) + { + Debug.LogMessage(LogEventLevel.Information, "****All Devices Initalized****"); + + OnAllDevicesInitialized(); + } + } + + private static void OnAllDevicesActivated() + { + var handler = AllDevicesActivated; + if (handler != null) + { + handler(null, new EventArgs()); + } + } + + private static void OnAllDevicesRegistered() + { + var handler = AllDevicesRegistered; + if (handler != null) + { + handler(null, new EventArgs()); + } + } + + private static void OnAllDevicesInitialized() + { + var handler = AllDevicesInitialized; + if (handler != null) + { + handler(null, new EventArgs()); + } + } + + /// + /// Calls activate on all Device class items + /// + public static void DeactivateAll() + { + try + { + DeviceCriticalSection.Enter(); + foreach (var d in Devices.Values.OfType()) + { + d.Deactivate(); } } - - /// - /// DisableAllDeviceStreamDebugging method - /// - public static void DisableAllDeviceStreamDebugging() + finally { - foreach (var device in AllDevices) + DeviceCriticalSection.Leave(); + } + } + + //static void ListMethods(string devKey) + //{ + // var dev = GetDeviceForKey(devKey); + // if(dev != null) + // { + // var type = dev.GetType().GetType(); + // var methods = type.GetMethods(BindingFlags.Public|BindingFlags.Instance); + // var sb = new StringBuilder(); + // sb.AppendLine(string.Format("{2} methods on [{0}] ({1}):", dev.Key, type.Name, methods.Length)); + // foreach (var m in methods) + // { + // sb.Append(string.Format("{0}(", m.Name)); + // var pars = m.GetParameters(); + // foreach (var p in pars) + // sb.Append(string.Format("({1}){0} ", p.Name, p.ParameterType.Name)); + // sb.AppendLine(")"); + // } + // CrestronConsole.ConsoleCommandResponse(sb.ToString()); + // } + //} + + private static void ListDevices(string s) + { + Debug.LogMessage(LogEventLevel.Information, "{0} Devices registered with Device Manager:", Devices.Count); + var sorted = Devices.Values.ToList(); + sorted.Sort((a, b) => a.Key.CompareTo(b.Key)); + + foreach (var d in sorted) + { + var name = d is IKeyName ? (d as IKeyName).Name : "---"; + Debug.LogMessage(LogEventLevel.Information, " [{0}] {1}", d.Key, name); + } + } + + private static void ListDeviceFeedbacks(string devKey) + { + var dev = GetDeviceForKey(devKey); + if (dev == null) + { + Debug.LogMessage(LogEventLevel.Information, "Device '{0}' not found", devKey); + return; + } + if (!(dev is IHasFeedback statusDev)) + { + Debug.LogMessage(LogEventLevel.Information, "Device '{0}' does not have visible feedbacks", devKey); + return; + } + statusDev.DumpFeedbacksToConsole(true); + } + + //static void ListDeviceCommands(string devKey) + //{ + // var dev = GetDeviceForKey(devKey); + // if (dev == null) + // { + // Debug.LogMessage(LogEventLevel.Information, "Device '{0}' not found", devKey); + // return; + // } + // Debug.LogMessage(LogEventLevel.Information, "This needs to be reworked. Stay tuned.", devKey); + //} + + private static void ListDeviceCommStatuses(string input) + { + + foreach (var dev in Devices.Values.OfType()) + { + CrestronConsole.ConsoleCommandResponse($"{dev}: {dev.CommunicationMonitor.Status}\r\n"); + } + } + + + //static void DoDeviceCommand(string command) + //{ + // Debug.LogMessage(LogEventLevel.Information, "Not yet implemented. Stay tuned"); + //} + + public static void AddDevice(IKeyed newDev) + { + try + { + if (!DeviceCriticalSection.TryEnter()) { - if (device is IStreamDebugging streamDevice) + Debug.LogMessage(LogEventLevel.Information, "Currently unable to add devices to Device Manager. Please try again"); + return; + } + // Check for device with same key + //var existingDevice = _Devices.FirstOrDefault(d => d.Key.Equals(newDev.Key, StringComparison.OrdinalIgnoreCase)); + ////// If it exists, remove or warn?? + //if (existingDevice != null) + + if (!AddDeviceEnabled) + { + Debug.LogMessage(LogEventLevel.Information, "All devices have been activated. Adding new devices is not allowed."); + return; + } + + if (Devices.ContainsKey(newDev.Key)) + { + Debug.LogMessage(LogEventLevel.Information, newDev, "WARNING: A device with this key already exists. Not added to manager"); + return; + } + Devices.Add(newDev.Key, newDev); + //if (!(_Devices.Contains(newDev))) + // _Devices.Add(newDev); + + if (newDev is EssentialsDevice essentialsDev) + essentialsDev.Initialized += DeviceManager_Initialized; + } + finally + { + DeviceCriticalSection.Leave(); + } + } + + public static void AddDevice(IEnumerable devicesToAdd) + { + try + { + if (!DeviceCriticalSection.TryEnter()) + { + Debug.LogMessage(LogEventLevel.Information, + "Currently unable to add devices to Device Manager. Please try again"); + return; + } + if (!AddDeviceEnabled) + { + Debug.LogMessage(LogEventLevel.Information, + "All devices have been activated. Adding new devices is not allowed."); + return; + } + + foreach (var dev in devicesToAdd) + { + try { - streamDevice.StreamDebugging.SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting.Off); + Devices.Add(dev.Key, dev); } + catch (ArgumentException ex) + { + Debug.LogMessage(LogEventLevel.Information, "Error adding device with key {0} to Device Manager: {1}\r\nStack Trace: {2}", + dev.Key, ex.Message, ex.StackTrace); + } + } + } + finally + { + DeviceCriticalSection.Leave(); + } + } + + public static void RemoveDevice(IKeyed newDev) + { + try + { + DeviceCriticalSection.Enter(); + if (newDev == null) + return; + if (Devices.ContainsKey(newDev.Key)) + Devices.Remove(newDev.Key); + //if (_Devices.Contains(newDev)) + // _Devices.Remove(newDev); + else + Debug.LogMessage(LogEventLevel.Information, "Device manager: Device '{0}' does not exist in manager. Cannot remove", newDev.Key); + } + finally + { + DeviceCriticalSection.Leave(); + } + } + + public static IEnumerable GetDeviceKeys() + { + //return _Devices.Select(d => d.Key).ToList(); + return Devices.Keys; + } + + public static IEnumerable GetDevices() + { + //return _Devices.Select(d => d.Key).ToList(); + return Devices.Values; + } + + /// + /// GetDeviceForKey method + /// + public static IKeyed GetDeviceForKey(string key) + { + //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (key != null && Devices.ContainsKey(key)) + return Devices[key]; + + return null; + } + + /// + /// GetDeviceForKey method + /// + /// + public static T GetDeviceForKey(string key) + { + //return _Devices.FirstOrDefault(d => d.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (key == null || !Devices.ContainsKey(key)) + return default; + + if (!(Devices[key] is T)) + { + Debug.LogMessage(LogEventLevel.Error, "Device with key '{0}' is not of type '{1}'", key, typeof(T).Name); + return default; + } + + return (T)Devices[key]; + } + /// + /// Console handler that simulates com port data receive + /// + /// + public static void SimulateComReceiveOnDevice(string s) + { + // devcomsim:1 xyzabc + var match = Regex.Match(s, @"(\S*)\s*(.*)"); + if (match.Groups.Count < 3) + { + CrestronConsole.ConsoleCommandResponse(" Format: devsimreceive:P "); + return; + } + //Debug.LogMessage(LogEventLevel.Verbose, "**** {0} - {1} ****", match.Groups[1].Value, match.Groups[2].Value); + + if (!(GetDeviceForKey(match.Groups[1].Value) is ComPortController com)) + { + CrestronConsole.ConsoleCommandResponse("'{0}' is not a comm port device", match.Groups[1].Value); + return; + } + com.SimulateReceive(match.Groups[2].Value); + } + + /// + /// Prints a list of routing inputs and outputs by device key. + /// + /// Device key from which to report data + public static void GetRoutingPorts(string s) + { + var device = GetDeviceForKey(s); + + if (device == null) return; + var inputPorts = ((device as IRoutingInputs) != null) ? (device as IRoutingInputs).InputPorts : null; + var outputPorts = ((device as IRoutingOutputs) != null) ? (device as IRoutingOutputs).OutputPorts : null; + if (inputPorts != null) + { + CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine); + foreach (var routingInputPort in inputPorts) + { + CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine); + } + } + if (outputPorts == null) return; + CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine); + foreach (var routingOutputPort in outputPorts) + { + CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine); + } + } + + /// + /// Attempts to set the debug level of a device + /// + /// + public static void SetDeviceStreamDebugging(string s) + { + if (String.IsNullOrEmpty(s) || s.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse( + @"SETDEVICESTREAMDEBUG [{deviceKey}] [OFF |TX | RX | BOTH] [timeOutInMinutes] + {deviceKey} [OFF | TX | RX | BOTH] - Device to set stream debugging on, and which setting to use + timeOutInMinutes - Set timeout for stream debugging. Default is 30 minutes"); + return; + } + + var args = s.Split(' '); + + var deviceKey = args[0]; + var setting = args[1]; + + var timeout = String.Empty; + + if (args.Length >= 3) + { + timeout = args[2]; + } + + + if (!(GetDeviceForKey(deviceKey) is IStreamDebugging device)) + { + CrestronConsole.ConsoleCommandResponse("Unable to get device with key: {0}", deviceKey); + return; + } + + eStreamDebuggingSetting debugSetting; + + try + { + debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), setting, true); + } + catch + { + CrestronConsole.ConsoleCommandResponse("Unable to convert setting value. Please use off/rx/tx/both"); + return; + } + + if (!string.IsNullOrEmpty(timeout)) + { + try + { + var min = Convert.ToUInt32(timeout); + + device.StreamDebugging.SetDebuggingWithSpecificTimeout(debugSetting, min); + CrestronConsole.ConsoleCommandResponse("Device: '{0}' debug level set to {1} for {2} minutes", deviceKey, debugSetting, min); + + } + catch (Exception e) + { + CrestronConsole.ConsoleCommandResponse("Unable to convert minutes or settings value. Please use an integer value for minutes. Error: {0}", e); + } + } + else + { + device.StreamDebugging.SetDebuggingWithDefaultTimeout(debugSetting); + CrestronConsole.ConsoleCommandResponse("Device: '{0}' debug level set to {1} for default time (30 minutes)", deviceKey, debugSetting); + } + } + + /// + /// Sets stream debugging settings to off for all devices + /// + public static void DisableAllDeviceStreamDebugging() + { + foreach (var device in AllDevices) + { + if (device is IStreamDebugging streamDevice) + { + streamDevice.StreamDebugging.SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting.Off); } } } diff --git a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs index c79be592..6726e763 100644 --- a/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/DeviceFactory.cs @@ -10,273 +10,266 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.ComponentModel.DataAnnotations; +using System.IO; -namespace PepperDash.Essentials.Core +namespace PepperDash.Essentials.Core; + + +/// +/// Provides functionality for managing and registering device factories, including loading plugin-based factories and +/// retrieving devices based on their configuration. +/// +/// The class is responsible for discovering and registering device factories +/// from plugins, as well as providing methods to retrieve devices based on their configuration. It maintains a +/// collection of factory methods that are keyed by device type names, allowing for extensibility through plugins. This +/// class also handles metadata retrieval and secret management for device configurations. +public class DeviceFactory { /// - /// Provides functionality for managing and registering device factories, including loading plugin-based factories and - /// retrieving devices based on their configuration. + /// Initializes a new instance of the class and loads all available device factories + /// from the current assembly. /// - /// The class is responsible for discovering and registering device factories - /// from plugins, as well as providing methods to retrieve devices based on their configuration. It maintains a - /// collection of factory methods that are keyed by device type names, allowing for extensibility through plugins. This - /// class also handles metadata retrieval and secret management for device configurations. - public class DeviceFactory - { - /// - /// Initializes a new instance of the class and loads all available device factories - /// from the current assembly. - /// - /// This constructor scans the executing assembly for types that implement the interface and are not abstract or interfaces. For each valid type, an instance is - /// created and passed to the LoadDeviceFactories method for further processing. If a type cannot be - /// instantiated, an informational log message is generated, and the process continues with the remaining - /// types. - public DeviceFactory() - { - var programAssemblies = Directory.GetFiles(InitialParametersClass.ProgramDirectory.ToString(), "*.dll"); + /// This constructor scans the executing assembly for types that implement the interface and are not abstract or interfaces. For each valid type, an instance is + /// created and passed to the LoadDeviceFactories method for further processing. If a type cannot be + /// instantiated, an informational log message is generated, and the process continues with the remaining + /// types. + public DeviceFactory() + { + var programAssemblies = Directory.GetFiles(InitialParametersClass.ProgramDirectory.ToString(), "*.dll"); - // Assemblies known to cause load errors that should be skipped - var assembliesToSkip = new[] { "CrestronOnvif.dll" }; - - foreach (var assembly in programAssemblies) - { - if (assembliesToSkip.Any(a => Path.GetFileName(assembly).Equals(a, StringComparison.OrdinalIgnoreCase))) - { - Debug.LogMessage(LogEventLevel.Verbose, "Skipping assembly: {assemblyName}", Path.GetFileName(assembly)); - continue; - } - - try - { - Assembly.LoadFrom(assembly); - } - catch (Exception e) - { - Debug.LogError("Unable to load assembly: {assemblyName} - {message}", assembly, e.Message); - } - } - - var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - - // Loop through all loaded assemblies that contain at least 1 type that implements IDeviceFactory - foreach (var assembly in loadedAssemblies) - { - Debug.LogDebug("loaded assembly: {assemblyName}", assembly.GetName()?.Name ?? "Unknown"); - - PluginLoader.AddLoadedAssembly(assembly.GetName()?.Name ?? "Unknown", assembly); - - var types = assembly.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); - - if (types == null || !types.Any()) - { - Debug.LogDebug("No DeviceFactory types found in assembly: {assemblyName}", assembly.GetName().Name); - continue; - } - - foreach (var type in types) - { - try - { - var factory = (IDeviceFactory)Activator.CreateInstance(type); - LoadDeviceFactories(factory); - } - catch (Exception e) - { - Debug.LogError("Unable to load type: '{message}' DeviceFactory: {type}", e.Message, type.Name); - } - } - - } - } - - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IDeviceFactory deviceFactory) - { - foreach (var typeName in deviceFactory.TypeNames) - { - string description = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0 - ? descriptionAttribute[0].Description - : "No description available"; - - AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); - } - } - - /// - /// A dictionary of factory methods, keyed by config types, added by plugins. - /// These methods are looked up and called by GetDevice in this class. - /// - private static readonly Dictionary FactoryMethods = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// Registers a factory method for creating instances of a specific type. - /// - /// This method associates a type name with a factory method, allowing instances of the type to - /// be created dynamically. The factory method is stored internally and can be retrieved or invoked as - /// needed. - /// The name of the type for which the factory method is being registered. This value cannot be null or empty. - /// A delegate that defines the factory method. The delegate takes a parameter and - /// returns an instance of . - public static void AddFactoryForType(string typeName, Func method) - { - FactoryMethods.Add(typeName, new DeviceFactoryWrapper() { FactoryMethod = method }); - } - - /// - /// Registers a factory method for creating instances of a specific device type. - /// - /// If a factory method for the specified already exists, the method - /// will not overwrite it and will log an informational message instead. - /// The unique name of the device type. This serves as the key for identifying the factory method. - /// A brief description of the device type. This is used for informational purposes. - /// The of the device being registered. This represents the runtime type of the device. - /// A factory method that takes a as input and returns an instance of . - public static void AddFactoryForType(string typeName, string description, Type Type, Func method) - { - if (FactoryMethods.ContainsKey(typeName)) - { - Debug.LogInformation("Unable to add type: '{typeName}'. Already exists in DeviceFactory", typeName); - return; - } - - var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; - - FactoryMethods.Add(typeName, wrapper); - } - - private static void CheckForSecrets(IEnumerable obj) - { - foreach (var prop in obj.Where(prop => prop.Value as JObject != null)) - { - if (prop.Name.Equals("secret", StringComparison.CurrentCultureIgnoreCase)) - { - var secret = GetSecret(prop.Children().First().ToObject()); - - prop.Parent.Replace(secret); - } - - if (!(prop.Value is JObject recurseProp)) continue; - - CheckForSecrets(recurseProp.Properties()); - } - } - - private static string GetSecret(SecretsPropertiesConfig data) - { - var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); - if (secretProvider == null) return null; - var secret = secretProvider.GetSecret(data.Key); - if (secret != null) return (string)secret.Value; - Debug.LogMessage(LogEventLevel.Debug, - "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", - data.Provider, data.Key); - return string.Empty; - } - - - /// - /// Creates and returns a device instance based on the provided . - /// - /// This method attempts to create a device using the type specified in the - /// parameter. If the type corresponds to a registered factory method, the device is created and returned. If the - /// type is unrecognized or an exception occurs, the method logs the error and returns . - /// The configuration object containing the key, name, type, and properties required to create the device. - /// An instance of a device that implements , or if the device type is - /// not recognized or an error occurs during creation. - public static IKeyed GetDevice(DeviceConfig dc) + foreach(var assembly in programAssemblies) { try { - var localDc = new DeviceConfig(dc); - - var key = localDc.Key; - var name = localDc.Name; - var type = localDc.Type; - var properties = localDc.Properties; - - var typeName = localDc.Type.ToLower(); - - if (properties is JObject jObject) - { - var jProp = jObject.Properties(); - - CheckForSecrets(jProp); - } - - if (!FactoryMethods.TryGetValue(typeName, out var wrapper)) - { - Debug.LogWarning("Device type '{typeName}' not found in DeviceFactory", typeName); - return null; - } - - Debug.LogInformation("Loading '{type}' from {assemblyName}", typeName, wrapper.Type.Assembly.FullName); - - // Check for types that have been added by plugin dlls. - return wrapper.FactoryMethod(localDc); + Assembly.LoadFrom(assembly); } - catch (Exception ex) + catch (Exception e) { - Debug.LogError(ex, "Exception occurred while creating device {key}: {message}", dc.Key, ex.Message); - Debug.LogDebug(ex, "Exception details: {stackTrace}", ex.StackTrace); - return null; + Debug.LogError("Unable to load assembly: {assemblyName} - {message}", assembly, e.Message); } } - /// - /// Displays a list of device factory types that match the specified filter. - /// - /// The method outputs the filtered list of device factory types to the console, including their - /// key, type, and description. If a type is not specified by the plugin, it will be displayed as "Not Specified by - /// Plugin." - /// A string used to filter the device factory types by their keys. If the filter is null or empty, all device - /// factory types are displayed. - public static void GetDeviceFactoryTypes(string filter) + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + + // Loop through all loaded assemblies that contain at least 1 type that implements IDeviceFactory + foreach (var assembly in loadedAssemblies) { - var types = !string.IsNullOrEmpty(filter) - ? FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value) - : FactoryMethods; + Debug.LogDebug("loaded assembly: {assemblyName}", assembly.GetName().Name); - CrestronConsole.ConsoleCommandResponse("Device Types:"); + PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly); - foreach (var type in types.OrderBy(t => t.Key)) + var types = assembly.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + + if(types == null || !types.Any()) { - var description = type.Value.Description; - var Type = "Not Specified by Plugin"; - - if (type.Value.Type != null) - { - Type = type.Value.Type.FullName; - } - - CrestronConsole.ConsoleCommandResponse( - "Type: '{0}'\r\n" + - " Type: '{1}'\r\n" + - " Description: {2}{3}", type.Key, Type, description, CrestronEnvironment.NewLine); + Debug.LogDebug("No DeviceFactory types found in assembly: {assemblyName}", assembly.GetName().Name); + continue; } - } - /// - /// Retrieves a dictionary of device factory wrappers, optionally filtered by a specified string. - /// - /// A string used to filter the dictionary keys. Only entries with keys containing the specified filter will be - /// included. If or empty, all entries are returned. - /// A dictionary where the keys are strings representing device identifiers and the values are instances. The dictionary may be empty if no entries match the filter. - public static Dictionary GetDeviceFactoryDictionary(string filter) + foreach (var type in types) + { + try + { + var factory = (IDeviceFactory)Activator.CreateInstance(type); + LoadDeviceFactories(factory); + } + catch (Exception e) + { + Debug.LogError("Unable to load type: '{message}' DeviceFactory: {type}", e.Message, type.Name); + } + } + + } + } + + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) { - return string.IsNullOrEmpty(filter) - ? FactoryMethods - : FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value); + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } + + /// + /// A dictionary of factory methods, keyed by config types, added by plugins. + /// These methods are looked up and called by GetDevice in this class. + /// + private static readonly Dictionary FactoryMethods = + new(StringComparer.OrdinalIgnoreCase); + + /// + /// Registers a factory method for creating instances of a specific type. + /// + /// This method associates a type name with a factory method, allowing instances of the type to + /// be created dynamically. The factory method is stored internally and can be retrieved or invoked as + /// needed. + /// The name of the type for which the factory method is being registered. This value cannot be null or empty. + /// A delegate that defines the factory method. The delegate takes a parameter and + /// returns an instance of . + public static void AddFactoryForType(string typeName, Func method) + { + FactoryMethods.Add(typeName, new DeviceFactoryWrapper() { FactoryMethod = method}); + } + + /// + /// Registers a factory method for creating instances of a specific device type. + /// + /// If a factory method for the specified already exists, the method + /// will not overwrite it and will log an informational message instead. + /// The unique name of the device type. This serves as the key for identifying the factory method. + /// A brief description of the device type. This is used for informational purposes. + /// The of the device being registered. This represents the runtime type of the device. + /// A factory method that takes a as input and returns an instance of . + public static void AddFactoryForType(string typeName, string description, Type Type, Func method) + { + if(FactoryMethods.ContainsKey(typeName)) + { + Debug.LogInformation("Unable to add type: '{typeName}'. Already exists in DeviceFactory", typeName); + return; + } + + var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; + + FactoryMethods.Add(typeName, wrapper); + } + + private static void CheckForSecrets(IEnumerable obj) + { + foreach (var prop in obj.Where(prop => prop.Value as JObject != null)) + { + if (prop.Name.Equals("secret", StringComparison.CurrentCultureIgnoreCase)) + { + var secret = GetSecret(prop.Children().First().ToObject()); + + prop.Parent.Replace(secret); + } + if (prop.Value is not JObject recurseProp) return; + CheckForSecrets(recurseProp.Properties()); + } + } + + private static string GetSecret(SecretsPropertiesConfig data) + { + var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); + if (secretProvider == null) return null; + var secret = secretProvider.GetSecret(data.Key); + if (secret != null) return (string) secret.Value; + Debug.LogMessage(LogEventLevel.Debug, + "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", + data.Provider, data.Key); + return string.Empty; + } + + + /// + /// Creates and returns a device instance based on the provided . + /// + /// This method attempts to create a device using the type specified in the + /// parameter. If the type corresponds to a registered factory method, the device is created and returned. If the + /// type is unrecognized or an exception occurs, the method logs the error and returns . + /// The configuration object containing the key, name, type, and properties required to create the device. + /// An instance of a device that implements , or if the device type is + /// not recognized or an error occurs during creation. + public static IKeyed GetDevice(DeviceConfig dc) + { + try + { + var localDc = new DeviceConfig(dc); + + var key = localDc.Key; + var name = localDc.Name; + var type = localDc.Type; + var properties = localDc.Properties; + + var typeName = localDc.Type.ToLower(); + + if (properties is JObject jObject) + { + var jProp = jObject.Properties(); + + CheckForSecrets(jProp); + } + + if (!FactoryMethods.TryGetValue(typeName, out var wrapper)) + { + Debug.LogWarning("Device type '{typeName}' not found in DeviceFactory", typeName); + return null; + } + + Debug.LogInformation("Loading '{type}' from {assemblyName}", typeName, wrapper.Type.Assembly.FullName); + + // Check for types that have been added by plugin dlls. + return wrapper.FactoryMethod(localDc); + } + catch (Exception ex) + { + Debug.LogError(ex, "Exception occurred while creating device {0}: {1}", null, dc.Key, ex.Message); + return null; + } + } + + /// + /// Displays a list of device factory types that match the specified filter. + /// + /// The method outputs the filtered list of device factory types to the console, including their + /// key, type, and description. If a type is not specified by the plugin, it will be displayed as "Not Specified by + /// Plugin." + /// A string used to filter the device factory types by their keys. If the filter is null or empty, all device + /// factory types are displayed. + public static void GetDeviceFactoryTypes(string filter) + { + var types = !string.IsNullOrEmpty(filter) + ? FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value) + : FactoryMethods; + + CrestronConsole.ConsoleCommandResponse("Device Types:"); + + foreach (var type in types.OrderBy(t => t.Key)) + { + var description = type.Value.Description; + var Type = "Not Specified by Plugin"; + + if (type.Value.Type != null) + { + Type = type.Value.Type.FullName; + } + + CrestronConsole.ConsoleCommandResponse( + @"Type: '{0}' + Type: '{1}' + Description: {2}{3}", type.Key, Type, description, CrestronEnvironment.NewLine); + } + } + + /// + /// Retrieves a dictionary of device factory wrappers, optionally filtered by a specified string. + /// + /// A string used to filter the dictionary keys. Only entries with keys containing the specified filter will be + /// included. If or empty, all entries are returned. + /// A dictionary where the keys are strings representing device identifiers and the values are instances. The dictionary may be empty if no entries match the filter. + public static Dictionary GetDeviceFactoryDictionary(string filter) + { + return string.IsNullOrEmpty(filter) + ? FactoryMethods + : FactoryMethods.Where(k => k.Key.Contains(filter)).ToDictionary(k => k.Key, k => k.Value); + } } diff --git a/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs b/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs index eb02f3e5..5a082209 100644 --- a/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs +++ b/src/PepperDash.Essentials.Core/Factory/ProcessorExtensionDeviceFactory.cs @@ -10,160 +10,143 @@ using System.Linq; namespace PepperDash.Essentials.Core; +public class ProcessorExtensionDeviceFactory +{ + public ProcessorExtensionDeviceFactory() { + var assy = Assembly.GetExecutingAssembly(); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); + + var extensions = assy.GetTypes().Where(ct => typeof(IProcessorExtensionDeviceFactory) + .IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + + if (extensions != null ) + { + foreach ( var extension in extensions ) + { + try + { + var factory = (IProcessorExtensionDeviceFactory)Activator.CreateInstance(extension); + factory.LoadFactories(); + } + catch( Exception e ) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to load extension device: '{1}' ProcessorExtensionDeviceFactory: {0}", e, extension.Name); + } + } + } + } + /// - /// Represents a ProcessorExtensionDeviceFactory + /// A dictionary of factory methods, keyed by config types, added by plugins. + /// These methods are looked up and called by GetDevice in this class. /// - public class ProcessorExtensionDeviceFactory + static Dictionary ProcessorExtensionFactoryMethods = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + + /// + /// Adds a plugin factory method + /// + /// + /// + public static void AddFactoryForType(string extensionName, Func method) { - /// - /// Constructor - /// - public ProcessorExtensionDeviceFactory() { - var assy = Assembly.GetExecutingAssembly(); - PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); + //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); + ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods.Add(extensionName, new DeviceFactoryWrapper() { FactoryMethod = method }); + } - var extensions = assy.GetTypes().Where(ct => typeof(IProcessorExtensionDeviceFactory) - .IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + public static void AddFactoryForType(string extensionName, string description, Type Type, Func method) + { + //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); - if (extensions != null ) + if (ProcessorExtensionFactoryMethods.ContainsKey(extensionName)) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to add extension device: '{0}'. Already exists in ProcessorExtensionDeviceFactory", extensionName); + return; + } + + var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; + ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods.Add(extensionName, wrapper); + } + + private static void CheckForSecrets(IEnumerable obj) + { + foreach (var prop in obj.Where(prop => prop.Value as Newtonsoft.Json.Linq.JObject != null)) + { + if (prop.Name.ToLower() == "secret") { - foreach ( var extension in extensions ) - { - try - { - var factory = (IProcessorExtensionDeviceFactory)Activator.CreateInstance(extension); - factory.LoadFactories(); - } - catch( Exception e ) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to load extension device: '{1}' ProcessorExtensionDeviceFactory: {0}", e, extension.Name); - } - } + var secret = GetSecret(prop.Children().First().ToObject()); + //var secret = GetSecret(JsonConvert.DeserializeObject(prop.Children().First().ToString())); + prop.Parent.Replace(secret); } + var recurseProp = prop.Value as Newtonsoft.Json.Linq.JObject; + if (recurseProp == null) return; + CheckForSecrets(recurseProp.Properties()); } + } - /// - /// A dictionary of factory methods, keyed by config types, added by plugins. - /// These methods are looked up and called by GetDevice in this class. - /// - static Dictionary ProcessorExtensionFactoryMethods = - new Dictionary(StringComparer.OrdinalIgnoreCase); + private static string GetSecret(SecretsPropertiesConfig data) + { + var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); + if (secretProvider == null) return null; + var secret = secretProvider.GetSecret(data.Key); + if (secret != null) return (string)secret.Value; + Debug.LogMessage(LogEventLevel.Debug, + "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", + data.Provider, data.Key); + return String.Empty; + } - - /// - /// Adds a plugin factory method - /// - /// name fo extension to add - /// method to add - /// - public static void AddFactoryForType(string extensionName, Func method) + /// + /// The factory method for processor extension devices. Also iterates the Factory methods that have + /// been loaded from plugins + /// + /// + /// + public static IKeyed GetExtensionDevice(DeviceConfig dc) + { + try { - //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); - ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods.Add(extensionName, new DeviceFactoryWrapper() { FactoryMethod = method }); - } + Debug.LogMessage(LogEventLevel.Information, "Loading '{0}' from Essentials Core", dc.Type); - /// - /// Adds a plugin factory method with type and description - /// - /// name of extension to add - /// description of extension to add - /// type of extension to add - /// method to add - public static void AddFactoryForType(string extensionName, string description, Type Type, Func method) - { - //Debug.LogMessage(LogEventLevel.Debug, "Adding factory method for type '{0}'", typeName); + var localDc = new DeviceConfig(dc); - if (ProcessorExtensionFactoryMethods.ContainsKey(extensionName)) + var key = localDc.Key; + var name = localDc.Name; + var type = localDc.Type; + var properties = localDc.Properties; + //var propRecurse = properties; + + var typeName = localDc.Type.ToLower(); + + var jObject = properties as Newtonsoft.Json.Linq.JObject; + if (jObject != null) { - Debug.LogMessage(LogEventLevel.Information, "Unable to add extension device: '{0}'. Already exists in ProcessorExtensionDeviceFactory", extensionName); - return; + var jProp = jObject.Properties(); + + CheckForSecrets(jProp); } - var wrapper = new DeviceFactoryWrapper() { Type = Type, Description = description, FactoryMethod = method }; - ProcessorExtensionDeviceFactory.ProcessorExtensionFactoryMethods.Add(extensionName, wrapper); + Debug.LogMessage(LogEventLevel.Verbose, "typeName = {0}", typeName); + // Check for types that have been added by plugin dlls. + return !ProcessorExtensionFactoryMethods.ContainsKey(typeName) ? null : ProcessorExtensionFactoryMethods[typeName].FactoryMethod(localDc); } - - private static void CheckForSecrets(IEnumerable obj) + catch (Exception ex) { - foreach (var prop in obj.Where(prop => prop.Value as Newtonsoft.Json.Linq.JObject != null)) + Debug.LogMessage(LogEventLevel.Information, "Exception occurred while creating device {0}: {1}", dc.Key, ex.Message); + + Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.StackTrace); + + if (ex.InnerException == null) { - if (prop.Name.ToLower() == "secret") - { - var secret = GetSecret(prop.Children().First().ToObject()); - //var secret = GetSecret(JsonConvert.DeserializeObject(prop.Children().First().ToString())); - prop.Parent.Replace(secret); - } - var recurseProp = prop.Value as Newtonsoft.Json.Linq.JObject; - if (recurseProp == null) return; - CheckForSecrets(recurseProp.Properties()); - } - } - - private static string GetSecret(SecretsPropertiesConfig data) - { - var secretProvider = SecretsManager.GetSecretProviderByKey(data.Provider); - if (secretProvider == null) return null; - var secret = secretProvider.GetSecret(data.Key); - if (secret != null) return (string)secret.Value; - Debug.LogMessage(LogEventLevel.Debug, - "Unable to retrieve secret {0}{1} - Make sure you've added it to the secrets provider", - data.Provider, data.Key); - return String.Empty; - } - - /// - /// The factory method for processor extension devices. Also iterates the Factory methods that have - /// been loaded from plugins - /// - /// - /// - /// - /// GetExtensionDevice method - /// - public static IKeyed GetExtensionDevice(DeviceConfig dc) - { - try - { - Debug.LogMessage(LogEventLevel.Information, "Loading '{0}' from Essentials Core", dc.Type); - - var localDc = new DeviceConfig(dc); - - var key = localDc.Key; - var name = localDc.Name; - var type = localDc.Type; - var properties = localDc.Properties; - //var propRecurse = properties; - - var typeName = localDc.Type.ToLower(); - - var jObject = properties as Newtonsoft.Json.Linq.JObject; - if (jObject != null) - { - var jProp = jObject.Properties(); - - CheckForSecrets(jProp); - } - - Debug.LogMessage(LogEventLevel.Verbose, "typeName = {0}", typeName); - // Check for types that have been added by plugin dlls. - return !ProcessorExtensionFactoryMethods.ContainsKey(typeName) ? null : ProcessorExtensionFactoryMethods[typeName].FactoryMethod(localDc); - } - catch (Exception ex) - { - Debug.LogMessage(LogEventLevel.Information, "Exception occurred while creating device {0}: {1}", dc.Key, ex.Message); - - Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.StackTrace); - - if (ex.InnerException == null) - { - return null; - } - - Debug.LogMessage(LogEventLevel.Information, "Inner exception while creating device {0}: {1}", dc.Key, - ex.InnerException.Message); - Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.InnerException.StackTrace); return null; } + + Debug.LogMessage(LogEventLevel.Information, "Inner exception while creating device {0}: {1}", dc.Key, + ex.InnerException.Message); + Debug.LogMessage(LogEventLevel.Verbose, "{0}", ex.InnerException.StackTrace); + return null; + } } } diff --git a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs index 3a3117cb..53f3927f 100644 --- a/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs +++ b/src/PepperDash.Essentials.Devices.Common/DeviceFactory.cs @@ -7,58 +7,51 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using Serilog.Events; -namespace PepperDash.Essentials.Devices.Common +namespace PepperDash.Essentials.Devices.Common; + +public class DeviceFactory { - /// - /// Represents a DeviceFactory - /// - public class DeviceFactory + + public DeviceFactory() { - /// - /// Initializes a new instance of the DeviceFactory class - /// - public DeviceFactory() + var assy = Assembly.GetExecutingAssembly(); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); + + var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + + if (types != null) { - var assy = Assembly.GetExecutingAssembly(); - PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); - - var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); - - if (types != null) + foreach (var type in types) { - foreach (var type in types) + try { - try - { - var factory = (IDeviceFactory)Activator.CreateInstance(type); - LoadDeviceFactories(factory); - } - catch (Exception e) - { - Debug.LogMessage(LogEventLevel.Information, "Unable to load type: '{1}' DeviceFactory: {0}", e, type.Name); - } + var factory = (IDeviceFactory)Activator.CreateInstance(type); + LoadDeviceFactories(factory); + } + catch (Exception e) + { + Debug.LogMessage(LogEventLevel.Information, "Unable to load type: '{1}' DeviceFactory: {0}", e, type.Name); } } } + } - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) { - foreach (var typeName in deviceFactory.TypeNames) - { - //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); - string description = (deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description - : "No description available"; - - Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); - } + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index 9ce584f3..cba0afd5 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -4,61 +4,54 @@ using System.Reflection; using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +public class MobileControlFactory { - /// - /// Factory class for the Mobile Control App Controller - /// - public class MobileControlFactory + public MobileControlFactory() { - /// - /// Create an instance of the class. - /// - public MobileControlFactory() + var assembly = Assembly.GetExecutingAssembly(); + + PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly); + + var types = assembly.GetTypes().Where(t => typeof(IDeviceFactory).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); + + if (types == null) { - var assembly = Assembly.GetExecutingAssembly(); - - PluginLoader.AddLoadedAssembly(assembly.GetName().Name, assembly); - - var types = assembly.GetTypes().Where(t => typeof(IDeviceFactory).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); - - if (types == null) - { - return; - } - - foreach (var type in types) - { - try - { - var factory = (IDeviceFactory)Activator.CreateInstance(type); - - LoadDeviceFactories(factory); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Unable to load type '{type}' DeviceFactory: {factory}", null, type.Name); - } - } + return; } - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + foreach (var type in types) { - foreach (var typeName in deviceFactory.TypeNames) + try { - string description = (deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description - : "No description available"; // Default value if no DescriptionAttribute is found + var factory = (IDeviceFactory)Activator.CreateInstance(type); - DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); + LoadDeviceFactories(factory); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Unable to load type '{type}' DeviceFactory: {factory}", null, type.Name); } } } + + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) + { + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); + } + } } diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 608c362f..be37ff21 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -12,273 +12,269 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Routing; +using System.Threading; +using Timeout = Crestron.SimplSharp.Timeout; using Serilog.Events; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +/// +/// Represents the main control system for the application, providing initialization, configuration loading, and device +/// management functionality. +/// +/// This class extends and serves as the entry point for the control +/// system. It manages the initialization of devices, rooms, tie lines, and other system components. Additionally, it +/// provides methods for platform determination, configuration loading, and system teardown. +public class ControlSystem : CrestronControlSystem, ILoadConfig { + private HttpLogoServer LogoServer; + + private Timer _startTimer; + private ManualResetEventSlim _initializeEvent; + private const long StartupTime = 500; + /// - /// Main control system class that inherits from CrestronControlSystem and manages program lifecycle + /// Initializes a new instance of the class, setting up the system's global state and + /// dependencies. /// - public class ControlSystem : CrestronControlSystem, ILoadConfig + /// This constructor configures the control system by initializing key components such as the + /// device manager and secrets manager, and sets global properties like the maximum number of user threads and the + /// program initialization state. It also adjusts the error log's minimum debug level based on the device + /// platform. + public ControlSystem() + : base() + { - HttpLogoServer LogoServer; - - private CTimer _startTimer; - private CEvent _initializeEvent; - private const long StartupTime = 500; - - /// - /// Initializes a new instance of the ControlSystem class - /// - public ControlSystem() - : base() - + try { - - Thread.MaxNumberOfUserThreads = 400; + Crestron.SimplSharpPro.CrestronThread.Thread.MaxNumberOfUserThreads = 400; + Global.ControlSystem = this; DeviceManager.Initialize(this); SecretsManager.Initialize(); SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true; Debug.SetErrorLogMinimumDebugLevel(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? LogEventLevel.Warning : LogEventLevel.Verbose); - - // AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; + } + catch (Exception e) + { + Debug.LogError(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); + } + } + + /// + /// Initializes the control system and prepares it for operation. + /// + /// This method ensures that all devices in the system are properly registered and initialized + /// before the system is fully operational. If the control system is of a DMPS type, the method waits for all + /// devices to activate, allowing HD-BaseT DM endpoints to register before completing initialization. For non-DMPS + /// systems, initialization proceeds without waiting. + public override void InitializeSystem() + { + // If the control system is a DMPS type, we need to wait to exit this method until all devices have had time to activate + // to allow any HD-BaseT DM endpoints to register first. + bool preventInitializationComplete = Global.ControlSystemIsDmpsType; + if (preventInitializationComplete) + { + Debug.LogMessage(LogEventLevel.Debug, "******************* Initializing System **********************"); + + _startTimer = new Timer(StartSystem, preventInitializationComplete, StartupTime, Timeout.Infinite); + + _initializeEvent = new ManualResetEventSlim(false); + + DeviceManager.AllDevicesRegistered += (o, a) => + { + _initializeEvent.Set(); + }; + + _initializeEvent.Wait(30000); + + Debug.LogMessage(LogEventLevel.Debug, "******************* System Initialization Complete **********************"); + + SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; + } + else + { + _startTimer = new Timer(StartSystem, preventInitializationComplete, StartupTime, Timeout.Infinite); + } + } + + private void StartSystem(object preventInitialization) + { + DeterminePlatform(); + + // Print .NET runtime version + Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); + + if (Debug.DoNotLoadConfigOnNextBoot) + { + CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file", + ConsoleAccessLevelEnum.AccessOperator); } - private System.Reflection.Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) + CrestronConsole.AddNewConsoleCommand(PluginLoader.ReportAssemblyVersions, "reportversions", "Reports the versions of the loaded assemblies", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(Core.DeviceFactory.GetDeviceFactoryTypes, "gettypes", "Gets the device types that can be built. Accepts a filter string.", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(BridgeHelper.PrintJoinMap, "getjoinmap", "map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(BridgeHelper.JoinmapMarkdown, "getjoinmapmarkdown" + , "generate markdown of map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(s => { - var assemblyName = new System.Reflection.AssemblyName(args.Name).Name; - if (assemblyName == "PepperDash_Core") - { - return System.Reflection.Assembly.LoadFrom("PepperDashCore.dll"); - } + foreach (var tl in TieLineCollection.Default) + CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); + }, + "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); - if (assemblyName == "PepperDash_Essentials_Core") - { - return System.Reflection.Assembly.LoadFrom("PepperDash.Essentials.Core.dll"); - } + CrestronConsole.AddNewConsoleCommand(s => + { + CrestronConsole.ConsoleCommandResponse + ("Current running configuration. This is the merged system and template configuration" + CrestronEnvironment.NewLine); + CrestronConsole.ConsoleCommandResponse(Newtonsoft.Json.JsonConvert.SerializeObject + (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented)); + }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); - if (assemblyName == "Essentials Devices Common") - { - return System.Reflection.Assembly.LoadFrom("PepperDash.Essentials.Devices.Common.dll"); - } + CrestronConsole.AddNewConsoleCommand(s => + CrestronConsole.ConsoleCommandResponse( + "This system can be found at the following URLs:{2}" + + "System URL: {0}{2}" + + "Template URL: {1}{2}", + ConfigReader.ConfigObject.SystemUrl, + ConfigReader.ConfigObject.TemplateUrl, + CrestronEnvironment.NewLine), + "portalinfo", + "Shows portal URLS from configuration", + ConsoleAccessLevelEnum.AccessOperator); - return null; + + CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, + "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); + + + if (!Debug.DoNotLoadConfigOnNextBoot) + { + GoWithLoad(); + return; } - /// - /// InitializeSystem method - /// - /// - public override void InitializeSystem() + if (!(bool)preventInitialization) { - // If the control system is a DMPS type, we need to wait to exit this method until all devices have had time to activate - // to allow any HD-BaseT DM endpoints to register first. - bool preventInitializationComplete = Global.ControlSystemIsDmpsType; - if (preventInitializationComplete) - { - Debug.LogMessage(LogEventLevel.Debug, "******************* InitializeSystem() Entering **********************"); + SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; + } + } - _startTimer = new CTimer(StartSystem, preventInitializationComplete, StartupTime); - _initializeEvent = new CEvent(true, false); - DeviceManager.AllDevicesRegistered += (o, a) => + /// + /// Determines if the program is running on a processor (appliance) or server (VC-4). + /// + /// Sets Global.FilePathPrefix and Global.ApplicationDirectoryPathPrefix based on platform + /// + public void DeterminePlatform() + { + try + { + Debug.LogMessage(LogEventLevel.Information, "Determining Platform..."); + + string filePathPrefix; + + var dirSeparator = Global.DirectorySeparator; + + string directoryPrefix; + + directoryPrefix = Directory.GetApplicationRootDirectory(); + + Global.SetAssemblyVersion(PluginLoader.GetAssemblyVersion(Assembly.GetExecutingAssembly())); + + if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Server) // Handles 3-series running Windows CE OS + { + string userFolder = "user"; + string nvramFolder = "nvram"; + + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, "4-series"); + //Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{0} on {1} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series"); + + // Check if User/ProgramX exists + if (Directory.Exists(Global.ApplicationDirectoryPathPrefix + dirSeparator + userFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber))) { - _initializeEvent.Set(); - }; - _initializeEvent.Wait(30000); - Debug.LogMessage(LogEventLevel.Debug, "******************* InitializeSystem() Exiting **********************"); - SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; + Debug.LogMessage(LogEventLevel.Information, "{userFolder:l}/program{applicationNumber} directory found", userFolder, InitialParametersClass.ApplicationNumber); + filePathPrefix = directoryPrefix + dirSeparator + userFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; + } + // Check if Nvram/Programx exists + else if (Directory.Exists(directoryPrefix + dirSeparator + nvramFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber))) + { + Debug.LogMessage(LogEventLevel.Information, "{nvramFolder:l}/program{applicationNumber} directory found", nvramFolder, InitialParametersClass.ApplicationNumber); + + filePathPrefix = directoryPrefix + dirSeparator + nvramFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; + } + // If neither exists, set path to User/ProgramX + else + { + Debug.LogMessage(LogEventLevel.Information, "{userFolder:l}/program{applicationNumber} directory found", userFolder, InitialParametersClass.ApplicationNumber); + + filePathPrefix = directoryPrefix + dirSeparator + userFolder + + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; + } + } + else // Handles Linux OS (Virtual Control) + { + //Debug.SetDebugLevel(2); + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on Virtual Control Server", Global.AssemblyVersion); + + // Set path to User/ + filePathPrefix = directoryPrefix + dirSeparator + "User" + dirSeparator; + } + + Global.SetFilePathPrefix(filePathPrefix); + } + catch (Exception e) + { + Debug.LogMessage(e, "Unable to determine platform due to exception"); + } + } + + /// + /// Begins the process of loading resources including plugins and configuration data + /// + public void GoWithLoad() + { + try + { + Debug.SetDoNotLoadConfigOnNextBoot(false); + + PluginLoader.AddProgramAssemblies(); + + _ = new Core.DeviceFactory(); + + Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); + + var filesReady = SetupFilesystem(); + if (filesReady) + { + Debug.LogMessage(LogEventLevel.Information, "Checking for plugins"); + PluginLoader.LoadPlugins(); + + Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config..."); + if (!ConfigReader.LoadConfig2()) + { + Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors"); + return; + } + + Load(); + Debug.LogMessage(LogEventLevel.Information, "Essentials load complete"); } else { - _startTimer = new CTimer(StartSystem, preventInitializationComplete, StartupTime); - } - } - - private void StartSystem(object preventInitialization) - { - DeterminePlatform(); - - // Print .NET runtime version - Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); - - if (Debug.DoNotLoadConfigOnNextBoot) - { - CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file", - ConsoleAccessLevelEnum.AccessOperator); - } - - CrestronConsole.AddNewConsoleCommand(PluginLoader.ReportAssemblyVersions, "reportversions", "Reports the versions of the loaded assemblies", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(Core.DeviceFactory.GetDeviceFactoryTypes, "gettypes", "Gets the device types that can be built. Accepts a filter string.", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(BridgeHelper.PrintJoinMap, "getjoinmap", "map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(BridgeHelper.JoinmapMarkdown, "getjoinmapmarkdown" - , "generate markdown of map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => - { - foreach (var tl in TieLineCollection.Default) - CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); - }, - "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => - { - CrestronConsole.ConsoleCommandResponse - ("Current running configuration. This is the merged system and template configuration" + CrestronEnvironment.NewLine); - CrestronConsole.ConsoleCommandResponse(Newtonsoft.Json.JsonConvert.SerializeObject - (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n")); - }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); - - CrestronConsole.AddNewConsoleCommand(s => - CrestronConsole.ConsoleCommandResponse( - "This system can be found at the following URLs:{2}" + - "System URL: {0}{2}" + - "Template URL: {1}{2}", - ConfigReader.ConfigObject.SystemUrl, - ConfigReader.ConfigObject.TemplateUrl, - CrestronEnvironment.NewLine), - "portalinfo", - "Shows portal URLS from configuration", - ConsoleAccessLevelEnum.AccessOperator); - - - CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, - "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); - - //DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); - - if (!Debug.DoNotLoadConfigOnNextBoot) - { - GoWithLoad(); - return; - } - - if (!(bool)preventInitialization) - { - SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; - } - } - - /// - /// DeterminePlatform method - /// - public void DeterminePlatform() - { - try - { - Debug.LogMessage(LogEventLevel.Information, "Determining Platform..."); - - string filePathPrefix; - - var dirSeparator = Global.DirectorySeparator; - - string directoryPrefix; - - directoryPrefix = Directory.GetApplicationRootDirectory(); - - Global.SetAssemblyVersion(PluginLoader.GetAssemblyVersion(Assembly.GetExecutingAssembly())); - - if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Server) // Handles 3-series running Windows CE OS - { - string userFolder = "user"; - string nvramFolder = "nvram"; - - Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, "4-series"); - //Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{0} on {1} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series"); - - // Check if User/ProgramX exists - if (Directory.Exists(Global.ApplicationDirectoryPathPrefix + dirSeparator + userFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber))) - { - - Debug.LogMessage(LogEventLevel.Information, "{userFolder:l}/program{applicationNumber} directory found", userFolder, InitialParametersClass.ApplicationNumber); - filePathPrefix = directoryPrefix + dirSeparator + userFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; - } - // Check if Nvram/Programx exists - else if (Directory.Exists(directoryPrefix + dirSeparator + nvramFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber))) - { - Debug.LogMessage(LogEventLevel.Information, "{nvramFolder:l}/program{applicationNumber} directory found", nvramFolder, InitialParametersClass.ApplicationNumber); - - filePathPrefix = directoryPrefix + dirSeparator + nvramFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; - } - // If neither exists, set path to User/ProgramX - else - { - Debug.LogMessage(LogEventLevel.Information, "{userFolder:l}/program{applicationNumber} directory found", userFolder, InitialParametersClass.ApplicationNumber); - - filePathPrefix = directoryPrefix + dirSeparator + userFolder - + dirSeparator + string.Format("program{0}", InitialParametersClass.ApplicationNumber) + dirSeparator; - } - } - else // Handles Linux OS (Virtual Control) - { - //Debug.SetDebugLevel(2); - Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on Virtual Control Server", Global.AssemblyVersion); - - // Set path to User/ - filePathPrefix = directoryPrefix + dirSeparator + "User" + dirSeparator; - } - - Global.SetFilePathPrefix(filePathPrefix); - } - catch (Exception e) - { - Debug.LogMessage(e, "Unable to determine platform due to exception"); - } - } - - /// - /// GoWithLoad method - /// - public void GoWithLoad() - { - try - { - Debug.SetDoNotLoadConfigOnNextBoot(false); - - PluginLoader.AddProgramAssemblies(); - - _ = new Core.DeviceFactory(); - // _ = new Devices.Common.DeviceFactory(); - // _ = new DeviceFactory(); - - // _ = new ProcessorExtensionDeviceFactory(); - // _ = new MobileControlFactory(); - - LoadAssets(); - - Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); - - var filesReady = SetupFilesystem(); - if (filesReady) - { - Debug.LogMessage(LogEventLevel.Information, "Checking for plugins"); - PluginLoader.LoadPlugins(); - - Debug.LogMessage(LogEventLevel.Information, "Folder structure verified. Loading config..."); - if (!ConfigReader.LoadConfig2()) - { - Debug.LogMessage(LogEventLevel.Information, "Essentials Load complete with errors"); - return; - } - - Load(); - Debug.LogMessage(LogEventLevel.Information, "Essentials load complete"); - } - else - { - Debug.LogMessage(LogEventLevel.Information, - @"---------------------------------------------- + Debug.LogMessage(LogEventLevel.Information, + @"---------------------------------------------- ------------------------------------------------ ------------------------------------------------ Essentials file structure setup completed. @@ -287,415 +283,415 @@ namespace PepperDash.Essentials ------------------------------------------------ ------------------------------------------------ ------------------------------------------------"); - } - - } - catch (Exception e) - { - Debug.LogMessage(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); - } - finally - { - // Notify the OS that the program intitialization has completed - SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; } } - - - - /// - /// Verifies filesystem is set up. IR, SGD, and programX folders - /// - bool SetupFilesystem() + catch (Exception e) { - Debug.LogMessage(LogEventLevel.Information, "Verifying and/or creating folder structure"); - var configDir = Global.FilePathPrefix; - - Debug.LogMessage(LogEventLevel.Information, "FilePathPrefix: {filePathPrefix:l}", configDir); - var configExists = Directory.Exists(configDir); - if (!configExists) - Directory.Create(configDir); - - var irDir = Global.FilePathPrefix + "ir"; - if (!Directory.Exists(irDir)) - Directory.Create(irDir); - - var sgdDir = Global.FilePathPrefix + "sgd"; - if (!Directory.Exists(sgdDir)) - Directory.Create(sgdDir); - - var pluginDir = Global.FilePathPrefix + "plugins"; - if (!Directory.Exists(pluginDir)) - Directory.Create(pluginDir); - - var joinmapDir = Global.FilePathPrefix + "joinmaps"; - if (!Directory.Exists(joinmapDir)) - Directory.Create(joinmapDir); - - return configExists; + Debug.LogMessage(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); + } + finally + { + // Notify the OS that the program intitialization has completed + SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; } - /// - /// TearDown method - /// - public void TearDown() - { - Debug.LogMessage(LogEventLevel.Information, "Tearing down existing system"); - DeviceManager.DeactivateAll(); - - TieLineCollection.Default.Clear(); - - foreach (var key in DeviceManager.GetDevices()) - DeviceManager.RemoveDevice(key); - - Debug.LogMessage(LogEventLevel.Information, "Tear down COMPLETE"); - } + } - /// - /// Load method - /// - void Load() - { - LoadDevices(); - LoadRooms(); - LoadLogoServer(); - DeviceManager.ActivateAll(); + /// + /// Verifies filesystem is set up. IR, SGD, and programX folders + /// + bool SetupFilesystem() + { + Debug.LogMessage(LogEventLevel.Information, "Verifying and/or creating folder structure"); + var configDir = Global.FilePathPrefix; - LoadTieLines(); + Debug.LogMessage(LogEventLevel.Information, "FilePathPrefix: {filePathPrefix:l}", configDir); + var configExists = Directory.Exists(configDir); + if (!configExists) + Directory.Create(configDir); - /*var mobileControl = GetMobileControlDevice(); + var irDir = Global.FilePathPrefix + "ir"; + if (!Directory.Exists(irDir)) + Directory.Create(irDir); + + var sgdDir = Global.FilePathPrefix + "sgd"; + if (!Directory.Exists(sgdDir)) + Directory.Create(sgdDir); + + var pluginDir = Global.FilePathPrefix + "plugins"; + if (!Directory.Exists(pluginDir)) + Directory.Create(pluginDir); + + var joinmapDir = Global.FilePathPrefix + "joinmaps"; + if (!Directory.Exists(joinmapDir)) + Directory.Create(joinmapDir); + + return configExists; + } + + /// + /// TearDown method + /// + public void TearDown() + { + Debug.LogMessage(LogEventLevel.Information, "Tearing down existing system"); + DeviceManager.DeactivateAll(); + + TieLineCollection.Default.Clear(); + + foreach (var key in DeviceManager.GetDevices()) + DeviceManager.RemoveDevice(key); + + Debug.LogMessage(LogEventLevel.Information, "Tear down COMPLETE"); + } + + + /// + /// Load method + /// + void Load() + { + LoadDevices(); + LoadRooms(); + LoadLogoServer(); + + DeviceManager.ActivateAll(); + + LoadTieLines(); + + /*var mobileControl = GetMobileControlDevice(); if (mobileControl == null) return; - mobileControl.LinkSystemMonitorToAppServer();*/ + mobileControl.LinkSystemMonitorToAppServer();*/ + } + + /// + /// Reads all devices from config and adds them to DeviceManager + /// + public void LoadDevices() + { + + // Build the processor wrapper class + DeviceManager.AddDevice(new Core.Devices.CrestronProcessor("processor")); + + DeviceManager.AddDevice(new RoutingFeedbackManager($"routingFeedbackManager", "Routing Feedback Manager")); + + // Add global System Monitor device + if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) + { + DeviceManager.AddDevice( + new Core.Monitoring.SystemMonitorController("systemMonitor")); } - /// - /// LoadDevices method - /// - public void LoadDevices() + foreach (var devConf in ConfigReader.ConfigObject.Devices) { + IKeyed newDev = null; - // Build the processor wrapper class - DeviceManager.AddDevice(new Core.Devices.CrestronProcessor("processor")); - - DeviceManager.AddDevice(new RoutingFeedbackManager($"routingFeedbackManager", "Routing Feedback Manager")); - - // Add global System Monitor device - if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) + try { - DeviceManager.AddDevice( - new Core.Monitoring.SystemMonitorController("systemMonitor")); - } - - foreach (var devConf in ConfigReader.ConfigObject.Devices) - { - IKeyed newDev = null; - - try + Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type); + // Skip this to prevent unnecessary warnings + if (devConf.Key == "processor") { - Debug.LogMessage(LogEventLevel.Information, "Creating device '{deviceKey:l}', type '{deviceType:l}'", devConf.Key, devConf.Type); - // Skip this to prevent unnecessary warnings - if (devConf.Key == "processor") - { - var prompt = Global.ControlSystem.ControllerPrompt; + var prompt = Global.ControlSystem.ControllerPrompt; - var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) || - string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase); + var typeMatch = string.Equals(devConf.Type, prompt, StringComparison.OrdinalIgnoreCase) || + string.Equals(devConf.Type, prompt.Replace("-", ""), StringComparison.OrdinalIgnoreCase); - if (!typeMatch) - Debug.LogMessage(LogEventLevel.Information, - "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", - devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); + if (!typeMatch) + Debug.LogMessage(LogEventLevel.Information, + "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", + devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); - continue; - } - - - if (newDev == null) - newDev = Core.DeviceFactory.GetDevice(devConf); - - if (newDev != null) - DeviceManager.AddDevice(newDev); - else - Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); - } - catch (Exception e) - { - Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); - } - } - Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded."); - - } - - - /// - /// LoadTieLines method - /// - public void LoadTieLines() - { - // In the future, we can't necessarily just clear here because devices - // might be making their own internal sources/tie lines - - var tlc = TieLineCollection.Default; - - if (ConfigReader.ConfigObject.TieLines == null) - { - return; - } - - foreach (var tieLineConfig in ConfigReader.ConfigObject.TieLines) - { - var newTL = tieLineConfig.GetTieLine(); - if (newTL != null) - tlc.Add(newTL); - } - - Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded."); - - } - - /// - /// LoadRooms method - /// - public void LoadRooms() - { - if (ConfigReader.ConfigObject.Rooms == null) - { - Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration."); - return; - } - - foreach (var roomConfig in ConfigReader.ConfigObject.Rooms) - { - try - { - var room = Core.DeviceFactory.GetDevice(roomConfig); - - if (room == null) - { - Debug.LogWarning("ERROR: Cannot load unknown room type '{roomType:l}', key '{roomKey:l}'.", roomConfig.Type, roomConfig.Key); - continue; - } - - DeviceManager.AddDevice(room); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Exception loading room {roomKey}:{roomType}", null, roomConfig.Key, roomConfig.Type); continue; } + + + if (newDev == null) + newDev = Core.DeviceFactory.GetDevice(devConf); + + if (newDev != null) + DeviceManager.AddDevice(newDev); + else + Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); } + catch (Exception e) + { + Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); + } + } + Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded."); - Debug.LogMessage(LogEventLevel.Information, "All Rooms Loaded."); + } + /// + /// Helper method to load tie lines. This should run after devices have loaded + /// + public void LoadTieLines() + { + // In the future, we can't necessarily just clear here because devices + // might be making their own internal sources/tie lines + + var tlc = TieLineCollection.Default; + + if (ConfigReader.ConfigObject.TieLines == null) + { + return; } - /// - /// Fires up a logo server if not already running - /// - void LoadLogoServer() + foreach (var tieLineConfig in ConfigReader.ConfigObject.TieLines) { - if (ConfigReader.ConfigObject.Rooms == null) - { - Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup."); - return; - } + var newTL = tieLineConfig.GetTieLine(); + if (newTL != null) + tlc.Add(newTL); + } - if ( - !ConfigReader.ConfigObject.Rooms.Any( - CheckRoomConfig)) - { - Debug.LogMessage(LogEventLevel.Information, "No rooms configured to use system Logo server. Bypassing Logo server startup"); - return; - } + Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded."); + } + + /// + /// Reads all rooms from config and adds them to DeviceManager + /// + public void LoadRooms() + { + if (ConfigReader.ConfigObject.Rooms == null) + { + Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration."); + return; + } + + foreach (var roomConfig in ConfigReader.ConfigObject.Rooms) + { try { - LogoServer = new HttpLogoServer(8080, Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo"); + var room = Core.DeviceFactory.GetDevice(roomConfig); + + if (room == null) + { + Debug.LogWarning("ERROR: Cannot load unknown room type '{roomType:l}', key '{roomKey:l}'.", roomConfig.Type, roomConfig.Key); + continue; + } + + DeviceManager.AddDevice(room); } - catch (Exception) + catch (Exception ex) { - Debug.LogMessage(LogEventLevel.Information, "NOTICE: Logo server cannot be started. Likely already running in another program"); + Debug.LogMessage(ex, "Exception loading room {roomKey}:{roomType}", null, roomConfig.Key, roomConfig.Type); + continue; } } - private bool CheckRoomConfig(DeviceConfig c) + Debug.LogMessage(LogEventLevel.Information, "All Rooms Loaded."); + + + } + + /// + /// Fires up a logo server if not already running + /// + void LoadLogoServer() + { + if (ConfigReader.ConfigObject.Rooms == null) { - string logoDark = null; - string logoLight = null; - string logo = null; + Debug.LogMessage(LogEventLevel.Information, "No rooms configured. Bypassing Logo server startup."); + return; + } - try + if ( + !ConfigReader.ConfigObject.Rooms.Any( + CheckRoomConfig)) + { + Debug.LogMessage(LogEventLevel.Information, "No rooms configured to use system Logo server. Bypassing Logo server startup"); + return; + } + + try + { + LogoServer = new HttpLogoServer(8080, Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo"); + } + catch (Exception) + { + Debug.LogMessage(LogEventLevel.Information, "NOTICE: Logo server cannot be started. Likely already running in another program"); + } + } + + private bool CheckRoomConfig(DeviceConfig c) + { + string logoDark = null; + string logoLight = null; + string logo = null; + + try + { + if (c.Properties["logoDark"] != null) { - if (c.Properties["logoDark"] != null) - { - logoDark = c.Properties["logoDark"].Value("type"); - } - - if (c.Properties["logoLight"] != null) - { - logoLight = c.Properties["logoLight"].Value("type"); - } - - if (c.Properties["logo"] != null) - { - logo = c.Properties["logo"].Value("type"); - } - - return ((logoDark != null && logoDark == "system") || - (logoLight != null && logoLight == "system") || (logo != null && logo == "system")); + logoDark = c.Properties["logoDark"].Value("type"); } - catch + + if (c.Properties["logoLight"] != null) { - Debug.LogMessage(LogEventLevel.Information, "Unable to find logo information in any room config"); - return false; + logoLight = c.Properties["logoLight"].Value("type"); + } + + if (c.Properties["logo"] != null) + { + logo = c.Properties["logo"].Value("type"); + } + + return ((logoDark != null && logoDark == "system") || + (logoLight != null && logoLight == "system") || (logo != null && logo == "system")); + } + catch + { + Debug.LogMessage(LogEventLevel.Information, "Unable to find logo information in any room config"); + return false; + } + } + + private static void LoadAssets() + { + var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); + Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.FilePathPrefix); + + var zipFiles = applicationDirectory.GetFiles("assets*.zip"); + + if (zipFiles.Length > 1) + { + throw new Exception("Multiple assets zip files found. Cannot continue."); + } + + if (zipFiles.Length == 1) + { + var zipFile = zipFiles[0]; + var assetsRoot = System.IO.Path.GetFullPath(Global.FilePathPrefix); + if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + { + assetsRoot += Path.DirectorySeparatorChar; + } + Debug.LogMessage(LogEventLevel.Information, "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName); + using (var archive = ZipFile.OpenRead(zipFile.FullName)) + { + foreach (var entry in archive.Entries) + { + var destinationPath = Path.Combine(Global.FilePathPrefix, entry.FullName); + var fullDest = System.IO.Path.GetFullPath(destinationPath); + if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + + if (string.IsNullOrEmpty(entry.Name)) + { + Directory.CreateDirectory(destinationPath); + continue; + } + + // If a directory exists where a file should go, delete it + if (Directory.Exists(destinationPath)) + Directory.Delete(destinationPath, true); + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); + entry.ExtractToFile(destinationPath, true); + Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); + } } } - private static void LoadAssets() + // cleaning up zip files + foreach (var file in zipFiles) { - var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix); - Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.FilePathPrefix); + File.Delete(file.FullName); + } - var zipFiles = applicationDirectory.GetFiles("assets*.zip"); + var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip"); - if (zipFiles.Length > 1) + if (htmlZipFiles.Length > 1) + { + throw new Exception("Multiple htmlassets zip files found in application directory. Please ensure only one htmlassets*.zip file is present and retry."); + } + + + if (htmlZipFiles.Length == 1) + { + var htmlZipFile = htmlZipFiles[0]; + var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + var userOrNvramDir = programDir.Parent; + var rootDir = userOrNvramDir?.Parent; + if (rootDir == null) { - throw new Exception("Multiple assets zip files found. Cannot continue."); + throw new Exception($"Unable to determine root directory for html extraction. Current path: {Global.FilePathPrefix}"); } - - if (zipFiles.Length == 1) + var htmlDir = Path.Combine(rootDir.FullName, "html"); + var htmlRoot = System.IO.Path.GetFullPath(htmlDir); + if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && + !htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) { - var zipFile = zipFiles[0]; - var assetsRoot = System.IO.Path.GetFullPath(Global.FilePathPrefix); - if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) + htmlRoot += Path.DirectorySeparatorChar; + } + Debug.LogMessage(LogEventLevel.Information, "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName); + using (var archive = ZipFile.OpenRead(htmlZipFile.FullName)) + { + foreach (var entry in archive.Entries) { - assetsRoot += Path.DirectorySeparatorChar; - } - Debug.LogMessage(LogEventLevel.Information, "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName); - using (var archive = ZipFile.OpenRead(zipFile.FullName)) - { - foreach (var entry in archive.Entries) + var destinationPath = Path.Combine(htmlDir, entry.FullName); + var fullDest = System.IO.Path.GetFullPath(destinationPath); + if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); + + if (string.IsNullOrEmpty(entry.Name)) { - var destinationPath = Path.Combine(Global.FilePathPrefix, entry.FullName); - var fullDest = System.IO.Path.GetFullPath(destinationPath); - if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); - - if (string.IsNullOrEmpty(entry.Name)) - { - Directory.CreateDirectory(destinationPath); - continue; - } - - // If a directory exists where a file should go, delete it - if (Directory.Exists(destinationPath)) - Directory.Delete(destinationPath, true); - - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - entry.ExtractToFile(destinationPath, true); - Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); + Directory.CreateDirectory(destinationPath); + continue; } + + // Only delete the file if it exists and is a file, not a directory + if (File.Exists(destinationPath)) + File.Delete(destinationPath); + + var parentDir = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(parentDir)) + Directory.CreateDirectory(parentDir); + + entry.ExtractToFile(destinationPath, true); + Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); } } + } - // cleaning up zip files - foreach (var file in zipFiles) + // cleaning up html zip files + foreach (var file in htmlZipFiles) + { + File.Delete(file.FullName); + } + + var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json"); + + if (jsonFiles.Length > 1) + { + Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", jsonFiles.Select(f => f.FullName).ToArray()); + throw new Exception("Multiple configuration files found. Cannot continue."); + } + + if (jsonFiles.Length == 1) + { + var jsonFile = jsonFiles[0]; + var finalPath = Path.Combine(Global.FilePathPrefix, jsonFile.Name); + Debug.LogMessage(LogEventLevel.Information, "Found configuration file: {jsonFile:l}... Moving to: {Destination}", jsonFile.FullName, finalPath); + + if (File.Exists(finalPath)) { - File.Delete(file.FullName); + Debug.LogMessage(LogEventLevel.Information, "Removing existing configuration file: {Destination}", finalPath); + File.Delete(finalPath); } - var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip"); - - if (htmlZipFiles.Length > 1) - { - throw new Exception("Multiple htmlassets zip files found in application directory. Please ensure only one htmlassets*.zip file is present and retry."); - } - - - if (htmlZipFiles.Length == 1) - { - var htmlZipFile = htmlZipFiles[0]; - var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); - var userOrNvramDir = programDir.Parent; - var rootDir = userOrNvramDir?.Parent; - if (rootDir == null) - { - throw new Exception($"Unable to determine root directory for html extraction. Current path: {Global.FilePathPrefix}"); - } - var htmlDir = Path.Combine(rootDir.FullName, "html"); - var htmlRoot = System.IO.Path.GetFullPath(htmlDir); - if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && - !htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString())) - { - htmlRoot += Path.DirectorySeparatorChar; - } - Debug.LogMessage(LogEventLevel.Information, "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName); - using (var archive = ZipFile.OpenRead(htmlZipFile.FullName)) - { - foreach (var entry in archive.Entries) - { - var destinationPath = Path.Combine(htmlDir, entry.FullName); - var fullDest = System.IO.Path.GetFullPath(destinationPath); - if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory."); - - if (string.IsNullOrEmpty(entry.Name)) - { - Directory.CreateDirectory(destinationPath); - continue; - } - - // Only delete the file if it exists and is a file, not a directory - if (File.Exists(destinationPath)) - File.Delete(destinationPath); - - var parentDir = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrEmpty(parentDir)) - Directory.CreateDirectory(parentDir); - - entry.ExtractToFile(destinationPath, true); - Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath); - } - } - } - - // cleaning up html zip files - foreach (var file in htmlZipFiles) - { - File.Delete(file.FullName); - } - - var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json"); - - if (jsonFiles.Length > 1) - { - Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", jsonFiles.Select(f => f.FullName).ToArray()); - throw new Exception("Multiple configuration files found. Cannot continue."); - } - - if (jsonFiles.Length == 1) - { - var jsonFile = jsonFiles[0]; - var finalPath = Path.Combine(Global.FilePathPrefix, jsonFile.Name); - Debug.LogMessage(LogEventLevel.Information, "Found configuration file: {jsonFile:l}... Moving to: {Destination}", jsonFile.FullName, finalPath); - - if (File.Exists(finalPath)) - { - Debug.LogMessage(LogEventLevel.Information, "Removing existing configuration file: {Destination}", finalPath); - File.Delete(finalPath); - } - - jsonFile.MoveTo(finalPath); - } + jsonFile.MoveTo(finalPath); } } } + diff --git a/src/PepperDash.Essentials/Factory/DeviceFactory.cs b/src/PepperDash.Essentials/Factory/DeviceFactory.cs index 36fca96a..dcfaf26e 100644 --- a/src/PepperDash.Essentials/Factory/DeviceFactory.cs +++ b/src/PepperDash.Essentials/Factory/DeviceFactory.cs @@ -7,58 +7,54 @@ using System.Reflection; using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials +namespace PepperDash.Essentials; + +/// +/// Responsible for loading all of the device types for this library +/// +public class DeviceFactory { - /// - /// Responsible for loading all of the device types for this library - /// - public class DeviceFactory + + public DeviceFactory() { + var assy = Assembly.GetExecutingAssembly(); + PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); - /// - /// Initializes a new instance of the DeviceFactory class and loads all device type factories - /// - public DeviceFactory() + var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); + + if (types != null) { - var assy = Assembly.GetExecutingAssembly(); - PluginLoader.AddLoadedAssembly(assy.GetName().Name, assy); - - var types = assy.GetTypes().Where(ct => typeof(IDeviceFactory).IsAssignableFrom(ct) && !ct.IsInterface && !ct.IsAbstract); - - if (types != null) + foreach (var type in types) { - foreach (var type in types) + try { - try - { - var factory = (IDeviceFactory)Activator.CreateInstance(type); - LoadDeviceFactories(factory); - } - catch (Exception e) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Unable to load type: '{exception}' DeviceFactory: {factoryName}", e, type.Name); - } + var factory = (IDeviceFactory)Activator.CreateInstance(type); + LoadDeviceFactories(factory); + } + catch (Exception e) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Unable to load type: '{exception}' DeviceFactory: {factoryName}", e, type.Name); } } } + } - /// - /// Loads device factories from the specified plugin device factory and registers them for use. - /// - /// This method retrieves metadata from the provided , including - /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type - /// names are converted to lowercase for registration. - /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. - private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + /// + /// Loads device factories from the specified plugin device factory and registers them for use. + /// + /// This method retrieves metadata from the provided , including + /// type names, descriptions, and configuration snippets, and registers the factory for each device type. The type + /// names are converted to lowercase for registration. + /// The plugin device factory that provides the device types, descriptions, and factory methods to be registered. + private static void LoadDeviceFactories(IDeviceFactory deviceFactory) + { + foreach (var typeName in deviceFactory.TypeNames) { - foreach (var typeName in deviceFactory.TypeNames) - { - string description = (deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) is DescriptionAttribute[] descriptionAttribute && descriptionAttribute.Length > 0) - ? descriptionAttribute[0].Description - : "No description available"; - - Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); - } + //Debug.LogMessage(LogEventLevel.Verbose, "Getting Description Attribute from class: '{0}'", typeof(T).FullName); + var descriptionAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(DescriptionAttribute), true) as DescriptionAttribute[]; + string description = descriptionAttribute[0].Description; + var snippetAttribute = deviceFactory.FactoryType.GetCustomAttributes(typeof(ConfigSnippetAttribute), true) as ConfigSnippetAttribute[]; + Core.DeviceFactory.AddFactoryForType(typeName.ToLower(), description, deviceFactory.FactoryType, deviceFactory.BuildDevice); } } } diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index 856b881f..d122c7b0 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -27,29 +27,6 @@ pdbonly bin\$(Configuration)\PepperDashEssentials.xml - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - -