using System; using System.Collections.Generic; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.Diagnostics; using PepperDash.Core; using PepperDash.Core.Abstractions; using PepperDash.Core.Adapters; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.Routing; using PepperDash.Essentials.Core.Web; using Serilog.Events; using Timeout = Crestron.SimplSharp.Timeout; namespace PepperDash.Essentials; /// /// 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, IInitializationExceptions { private Timer startTimer; private ManualResetEventSlim initializeEvent; private const long StartupTime = 500; /// /// List of exceptions that occurred during initialization. /// This can be used to report issues with loading devices or tie lines without crashing the entire system, /// which allows for partial functionality in cases where some components are misconfigured or have issues. /// public List InitializationExceptions { get; private set; } = new List(); /// /// Initializes a new instance of the class, setting up the system's global state and /// dependencies. /// /// 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() { try { // Register Crestron service adapters BEFORE the first reference to Debug, // so that Debug's static constructor uses these implementations instead of // calling the Crestron SDK statics directly. DebugServiceRegistration.Register( new CrestronEnvironmentAdapter(), new CrestronConsoleAdapter(), new CrestronDataStoreAdapter()); Crestron.SimplSharpPro.CrestronThread.Thread.MaxNumberOfUserThreads = 400; Global.ControlSystem = this; DeviceManager.Initialize(this); SecretsManager.Initialize(); SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true; Debug.SetErrorLogMinimumDebugLevel(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? LogEventLevel.Warning : LogEventLevel.Verbose); } catch (Exception e) { try { Debug.LogError(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); } catch { // Debug may not be initialized (e.g. its own static ctor failed); fall back to console. CrestronConsole.PrintLine($"FATAL INITIALIZE ERROR. System is in an inconsistent state\r\n{e.Message}\r\n{e.StackTrace}"); } } } /// /// 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) { try { DeterminePlatform(); // Print .NET runtime version Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); if (Debug.DoNotLoadConfigOnNextBoot) { CrestronConsole.AddNewConsoleCommand(s => Task.Run(() => GoWithLoad()), "go", "Loads configuration file", ConsoleAccessLevelEnum.AccessOperator); } CrestronConsole.AddNewConsoleCommand(PluginLoader.ReportAssemblyVersions, "reportversions", "Reports the versions of the loaded assemblies", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(Core.DeviceFactory.GetDeviceFactoryTypes, "gettypes", "Gets the device types that can be built. Accepts a filter string.", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(BridgeHelper.PrintJoinMap, "getjoinmap", "map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(BridgeHelper.JoinmapMarkdown, "getjoinmapmarkdown" , "generate markdown of map(s) for bridge or device on bridge [brKey [devKey]]", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(ListTieLines, "listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes", "Visualizes routes by signal type", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes", "Visualizes current active routes from DefaultCollection", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(s => { foreach (var tl in TieLineCollection.Default) CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); }, "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(s => { CrestronConsole.ConsoleCommandResponse ("Current running configuration. This is the merged system and template configuration" + CrestronEnvironment.NewLine); CrestronConsole.ConsoleCommandResponse(Newtonsoft.Json.JsonConvert.SerializeObject (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented)); }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand( PrintPortalInfo, "portalinfo", "Shows portal URLS from configuration", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts, "getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(PrintInitializationExceptions, "getinitexceptions", "Reports any exceptions that occurred during initialization", ConsoleAccessLevelEnum.AccessOperator); DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API")); if (!Debug.DoNotLoadConfigOnNextBoot) { GoWithLoad(); return; } if (!(bool)preventInitialization) { SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; } } catch (Exception e) { InitializationExceptions.Add(e); Debug.LogFatal(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); } } private void PrintPortalInfo(string args) { if (ConfigReader.ConfigObject == null) { CrestronConsole.ConsoleCommandResponse("No configuration loaded. Cannot show portal URLs."); return; } if (string.IsNullOrEmpty(ConfigReader.ConfigObject.SystemUrl) && string.IsNullOrEmpty(ConfigReader.ConfigObject.TemplateUrl)) { CrestronConsole.ConsoleCommandResponse("No portal URLs defined in config."); return; } CrestronConsole.ConsoleCommandResponse( "This system can be found at the following URLs:{2}" + "System URL: {0}{2}" + "Template URL: {1}{2}", ConfigReader.ConfigObject?.SystemUrl, ConfigReader.ConfigObject?.TemplateUrl, CrestronEnvironment.NewLine); } /// /// Determines if the program is running on a processor (appliance) or server (VC-4). /// /// 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))) { 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) { InitializationExceptions.Add(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(); LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix); 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.LoadConfig() || ConfigReader.ConfigObject == null) { Debug.LogMessage(LogEventLevel.Warning, "Unable to load config file."); } CheckPluginVersionsAgainstConfig(); Load(); Debug.LogMessage(LogEventLevel.Information, "Essentials load complete"); } else { Debug.LogMessage(LogEventLevel.Information, @"---------------------------------------------- ------------------------------------------------ ------------------------------------------------ Essentials file structure setup completed. Please load config, sgd and ir files and restart program. ------------------------------------------------ ------------------------------------------------ ------------------------------------------------"); } } catch (Exception e) { InitializationExceptions.Add(e); Debug.LogFatal(e, "FATAL INITIALIZE ERROR. System is in an inconsistent state"); } finally { // Notify the OS that the program intitialization has completed SystemMonitor.ProgramInitialization.ProgramInitializationComplete = true; } } private void CheckPluginVersionsAgainstConfig() { try { Debug.LogInformation("Checking plugin versions against config..."); if (ConfigReader.ConfigObject == null) return; var versions = ConfigReader.ConfigObject.Versions; if (versions == null) return; var pluginVersions = PluginLoader.EssentialsPluginAssemblies .Select(a => { var packageId = a.Assembly.GetCustomAttributes() .FirstOrDefault(attr => attr.Key == "NuGetPackageId")?.Value ?? a.Name; return (packageId, version: a.Version.ToString()); }) .ToDictionary(x => x.packageId, x => x.version); if (versions.Essentials != null) { if (pluginVersions.TryGetValue("PepperDashEssentials", out var pluginVersion)) { if (pluginVersion != versions.Essentials.PackageId) { Debug.LogMessage(LogEventLevel.Warning, "Essentials version mismatch. Config version: {configVersion:l}, Loaded plugin version: {pluginVersion:l}", versions.Essentials, pluginVersion); } } } if (versions.Packages == null) return; foreach (var version in versions.Packages) { if (pluginVersions.TryGetValue(version.PackageId, out var pluginVersion)) { if (pluginVersion != version.Version) { Debug.LogMessage(LogEventLevel.Warning, "Plugin version mismatch for {pluginKey:l}. Config version: {configVersion:l}, Loaded plugin version: {pluginVersion:l}", version.PackageId, version.Version, pluginVersion); } } else { Debug.LogMessage(LogEventLevel.Warning, "Plugin {pluginKey:l} specified in config versions but not found in loaded plugins.", version.PackageId); } } } catch (Exception ex) { InitializationExceptions.Add(ex); Debug.LogMessage(ex, "Error checking plugin versions against config. Continuing with load."); } } /// /// 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; 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; } /// /// 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(); DeviceManager.ActivateAll(); LoadTieLines(); } /// /// 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")); } if (ConfigReader.ConfigObject is null) { Debug.LogMessage(LogEventLevel.Warning, "LoadDevices: ConfigObject is null. Cannot load devices."); return; } Debug.LogInformation("Loading devices from config."); 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") { var prompt = Global.ControlSystem.ControllerPrompt; 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()); continue; } if (newDev == null) newDev = Core.DeviceFactory.GetDevice(devConf); if (newDev != null) DeviceManager.AddDevice(newDev); else Debug.LogMessage(LogEventLevel.Information, "ERROR: Cannot load unknown device type '{deviceType:l}', key '{deviceKey:l}'.", devConf.Type, devConf.Key); } catch (Exception e) { InitializationExceptions.Add(e); Debug.LogMessage(e, "ERROR: Creating device {deviceKey:l}. Skipping device.", args: new[] { devConf.Key }); } } Debug.LogMessage(LogEventLevel.Information, "All Devices Loaded."); return; } /// /// 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; } try { foreach (var tieLineConfig in ConfigReader.ConfigObject.TieLines) { var newTL = tieLineConfig.GetTieLine(); if (newTL != null) tlc.Add(newTL); } } catch (Exception e) { InitializationExceptions.Add(e); Debug.LogMessage(e, "ERROR: Creating tie line. Skipping tie line."); } Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded."); Extensions.MapDestinationsToSources(); Debug.LogMessage(LogEventLevel.Information, "All Routes Mapped."); } /// /// Visualizes routes in a tree format for better understanding of signal paths /// private void ListTieLines(string args) { try { if (args.Contains("?")) { CrestronConsole.ConsoleCommandResponse("Usage: listtielines [signaltype]\r\n"); CrestronConsole.ConsoleCommandResponse("Signal types: Audio, Video, SecondaryAudio, AudioVideo, UsbInput, UsbOutput\r\n"); return; } eRoutingSignalType? signalTypeFilter = null; if (!string.IsNullOrEmpty(args)) { eRoutingSignalType parsedType; if (Enum.TryParse(args.Trim(), true, out parsedType)) { signalTypeFilter = parsedType; } else { CrestronConsole.ConsoleCommandResponse("Invalid signal type: {0}\r\n", args.Trim()); CrestronConsole.ConsoleCommandResponse("Valid types: Audio, Video, SecondaryAudio, AudioVideo, UsbInput, UsbOutput\r\n"); return; } } var tielines = signalTypeFilter.HasValue ? TieLineCollection.Default.Where(tl => tl.Type.HasFlag(signalTypeFilter.Value)) : TieLineCollection.Default; var count = 0; foreach (var tl in tielines) { CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); count++; } CrestronConsole.ConsoleCommandResponse("\r\nTotal: {0} tieline{1}{2}", count, count == 1 ? "" : "s", CrestronEnvironment.NewLine); } catch (Exception ex) { CrestronConsole.ConsoleCommandResponse("Error listing tielines: {0}\r\n", ex.Message); } } private void VisualizeRoutes(string args) { try { if (args.Contains("?")) { CrestronConsole.ConsoleCommandResponse("Usage: visualizeroutes [signaltype] [-s source] [-d destination]\r\n"); CrestronConsole.ConsoleCommandResponse(" signaltype: Audio, Video, AudioVideo, etc.\r\n"); CrestronConsole.ConsoleCommandResponse(" -s: Filter by source key (partial match)\r\n"); CrestronConsole.ConsoleCommandResponse(" -d: Filter by destination key (partial match)\r\n"); return; } ParseRouteFilters(args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter); CrestronConsole.ConsoleCommandResponse("\r\n+===========================================================================+\r\n"); CrestronConsole.ConsoleCommandResponse("| ROUTE VISUALIZATION |\r\n"); CrestronConsole.ConsoleCommandResponse("+===========================================================================+\r\n\r\n"); foreach (var descriptorCollection in Extensions.RouteDescriptors.Where(kv => kv.Value.Descriptors.Count() > 0)) { // Filter by signal type if specified if (signalTypeFilter.HasValue && descriptorCollection.Key != signalTypeFilter.Value) continue; CrestronConsole.ConsoleCommandResponse("\r\n+--- Signal Type: {0} ({1} routes) ---\r\n", descriptorCollection.Key, descriptorCollection.Value.Descriptors.Count()); foreach (var descriptor in descriptorCollection.Value.Descriptors) { // Filter by source/dest if specified if (sourceFilter != null && !descriptor.Source.Key.ToLower().Contains(sourceFilter)) continue; if (destFilter != null && !descriptor.Destination.Key.ToLower().Contains(destFilter)) continue; VisualizeRouteDescriptor(descriptor); } } CrestronConsole.ConsoleCommandResponse("\r\n"); } catch (Exception ex) { CrestronConsole.ConsoleCommandResponse("Error visualizing routes: {0}\r\n", ex.Message); } } private void VisualizeCurrentRoutes(string args) { try { if (args.Contains("?")) { CrestronConsole.ConsoleCommandResponse("Usage: visualizecurrentroutes [signaltype] [-s source] [-d destination]\r\n"); CrestronConsole.ConsoleCommandResponse(" signaltype: Audio, Video, AudioVideo, etc.\r\n"); CrestronConsole.ConsoleCommandResponse(" -s: Filter by source key (partial match)\r\n"); CrestronConsole.ConsoleCommandResponse(" -d: Filter by destination key (partial match)\r\n"); return; } ParseRouteFilters(args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter); CrestronConsole.ConsoleCommandResponse("\r\n+===========================================================================+\r\n"); CrestronConsole.ConsoleCommandResponse("| CURRENT ROUTES VISUALIZATION |\r\n"); CrestronConsole.ConsoleCommandResponse("+===========================================================================+\r\n\r\n"); var hasRoutes = false; // Get all descriptors from DefaultCollection var allDescriptors = RouteDescriptorCollection.DefaultCollection.Descriptors; // Group by signal type var groupedByType = allDescriptors.GroupBy(d => d.SignalType); foreach (var group in groupedByType) { var signalType = group.Key; // Filter by signal type if specified if (signalTypeFilter.HasValue && signalType != signalTypeFilter.Value) continue; var filteredDescriptors = group.Where(d => { if (sourceFilter != null && !d.Source.Key.ToLower().Contains(sourceFilter)) return false; if (destFilter != null && !d.Destination.Key.ToLower().Contains(destFilter)) return false; return true; }).ToList(); if (filteredDescriptors.Count == 0) continue; hasRoutes = true; CrestronConsole.ConsoleCommandResponse("\r\n+--- Signal Type: {0} ({1} routes) ---\r\n", signalType, filteredDescriptors.Count); foreach (var descriptor in filteredDescriptors) { VisualizeRouteDescriptor(descriptor); } } if (!hasRoutes) { CrestronConsole.ConsoleCommandResponse("\r\nNo active routes found in current state.\r\n"); } CrestronConsole.ConsoleCommandResponse("\r\n"); } catch (Exception ex) { CrestronConsole.ConsoleCommandResponse("Error visualizing current state: {0}\r\n", ex.Message); } } /// /// Parses route filter arguments from command line /// /// Command line arguments /// Parsed signal type filter (if any) /// Parsed source filter (if any) /// Parsed destination filter (if any) private void ParseRouteFilters(string args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter) { signalTypeFilter = null; sourceFilter = null; destFilter = null; if (string.IsNullOrEmpty(args)) return; var parts = args.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < parts.Length; i++) { var part = parts[i]; // Check for flags if (part == "-s" && i + 1 < parts.Length) { sourceFilter = parts[++i].ToLower(); } else if (part == "-d" && i + 1 < parts.Length) { destFilter = parts[++i].ToLower(); } // Try to parse as signal type if not a flag and no signal type set yet else if (!part.StartsWith("-") && !signalTypeFilter.HasValue) { if (Enum.TryParse(part, true, out eRoutingSignalType parsedType)) { signalTypeFilter = parsedType; } } } } /// /// Visualizes a single route descriptor in a tree format /// private void VisualizeRouteDescriptor(RouteDescriptor descriptor) { CrestronConsole.ConsoleCommandResponse("|\r\n"); CrestronConsole.ConsoleCommandResponse("|-- {0} --> {1}\r\n", descriptor.Source.Key, descriptor.Destination.Key); if (descriptor.Routes == null || descriptor.Routes.Count == 0) { CrestronConsole.ConsoleCommandResponse("| +-- (No switching steps)\r\n"); return; } for (int i = 0; i < descriptor.Routes.Count; i++) { var route = descriptor.Routes[i]; var isLast = i == descriptor.Routes.Count - 1; var prefix = isLast ? "+" : "|"; var continuation = isLast ? " " : "|"; if (route.SwitchingDevice != null) { CrestronConsole.ConsoleCommandResponse("| {0}-- [{1}] {2}\r\n", prefix, route.SwitchingDevice.Key, GetSwitchDescription(route)); // Add visual connection line for non-last items if (!isLast) CrestronConsole.ConsoleCommandResponse("| {0} |\r\n", continuation); } else { CrestronConsole.ConsoleCommandResponse("| {0}-- {1}\r\n", prefix, route.ToString()); } } } /// /// Gets a readable description of the switching operation /// private string GetSwitchDescription(RouteSwitchDescriptor route) { if (route.OutputPort != null && route.InputPort != null) { return string.Format("{0} -> {1}", route.OutputPort.Key, route.InputPort.Key); } else if (route.InputPort != null) { return string.Format("-> {0}", route.InputPort.Key); } else { return "(passthrough)"; } } /// /// 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 { 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) { InitializationExceptions.Add(ex); Debug.LogMessage(ex, "Exception loading room {roomKey}:{roomType}", null, roomConfig.Key, roomConfig.Type); continue; } } Debug.LogMessage(LogEventLevel.Information, "All Rooms Loaded."); } private void PrintInitializationExceptions(string args) { if (args.Contains("?")) { CrestronConsole.ConsoleCommandResponse("Usage: getinitializationexceptions\r\n"); CrestronConsole.ConsoleCommandResponse("Reports any exceptions that occurred during initialization.\r\n"); return; } if (InitializationExceptions.Count == 0) { CrestronConsole.ConsoleCommandResponse("No initialization exceptions occurred.\r\n"); return; } CrestronConsole.ConsoleCommandResponse("Initialization Exceptions:\r\n"); foreach (var ex in InitializationExceptions) { CrestronConsole.ConsoleCommandResponse(" - {0}: {1}\r\n", ex.GetType().FullName, ex.Message); } } internal static void LoadAssets(string applicationDirectoryPath, string filePathPrefix) => AssetLoader.Load(applicationDirectoryPath, filePathPrefix); }